Skip to content

Commit bb15167

Browse files
committed
Pull request 85: AGDNS-3219-agdcslog-integration-tests
Merge in GO/adguarddnsclient from AGDNS-3219-agdcslog-integration-tests to master Squashed commit of the following: commit 5c8b471 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Mon Oct 6 17:25:49 2025 +0300 all: fix typo commit 588ef72 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Mon Oct 6 17:24:47 2025 +0300 agdcslog: imp naming commit edfb0c6 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Mon Oct 6 15:27:38 2025 +0300 agdcslog: add test commit fdca529 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Wed Oct 1 19:04:11 2025 +0300 all: ci set env commit b66055e Merge: 27b59b1 826395c Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Wed Oct 1 18:57:07 2025 +0300 Merge branch 'master' into AGDNS-3219-agdcslog-integration-tests commit 27b59b1 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Tue Sep 30 23:10:22 2025 +0300 agdcslog: imp tests commit ca1d219 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Mon Sep 29 22:53:16 2025 +0300 agdcslog: add tests commit 2febb2a Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Mon Sep 29 13:54:46 2025 +0300 agdcslog: integration tests
1 parent 826395c commit bb15167

File tree

6 files changed

+244
-0
lines changed

6 files changed

+244
-0
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
'runs-on': '${{ matrix.os }}'
1717
'env':
1818
'GO111MODULE': 'on'
19+
'TEST_AGDCSLOG': '1'
1920
'strategy':
2021
'fail-fast': false
2122
'matrix':

bamboo-specs/bamboo.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
'key': 'DNSCLIENT'
1010
'name': 'AdGuardDNSClient'
1111
'variables':
12+
# NOTE: The Test stage depends on BusyBox syslogd on Alpine.
1213
'dockerGo': 'adguard/go-builder:1.25.1--1'
1314
'maintainer': 'Adguard Go Team'
1415
'name': 'AdGuardDNSClient'
@@ -118,6 +119,16 @@
118119
119120
set -e -f -u -x
120121
122+
export TEST_AGDCSLOG=1
123+
124+
# Run syslogd in the foreground (-n) so its PID can be captured,
125+
# and enable the in-memory ring buffer (-C) so that logread can
126+
# read messages during integration tests.
127+
syslogd -n -C &
128+
129+
syslogd_pid="$!"
130+
readonly syslogd_pid
131+
121132
make \
122133
TEST_REPORTS_DIR="./test-reports/" \
123134
VERBOSE=1 \
@@ -129,6 +140,8 @@
129140
exit_code="$(cat ./test-reports/test-exit-code.txt)"
130141
readonly exit_code
131142
143+
kill "$syslogd_pid"
144+
132145
exit "$exit_code"
133146
134147
'Artifact':

internal/agdcslog/agdcslog_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import (
44
"bytes"
55
"context"
66
"log/slog"
7+
"os"
8+
"os/exec"
9+
"strconv"
710
"strings"
811
"sync"
912
"testing"
@@ -15,6 +18,88 @@ import (
1518
"github.com/stretchr/testify/require"
1619
)
1720

21+
// testTimeout is the common timeout for tests.
22+
const testTimeout = 1 * time.Second
23+
24+
// testServiceName is the service name for integration tests.
25+
const testServiceName = "AdGuardDNSClientTest"
26+
27+
// requireIntegration skips the test unless TEST_AGDCSLOG is set to "1".
28+
func requireIntegration(tb testing.TB) {
29+
tb.Helper()
30+
31+
const envName = "TEST_AGDCSLOG"
32+
33+
switch v := os.Getenv(envName); v {
34+
case "":
35+
tb.Skipf("skipping: %s is not set", envName)
36+
case "0":
37+
tb.Skip("skipping: integration tests are disabled")
38+
case "1":
39+
// Go on.
40+
default:
41+
tb.Skipf(`skipping: %s must be "1" or "0", got %q`, envName, v)
42+
}
43+
}
44+
45+
// requireExec skips the test if the executable is not in the PATH environment
46+
// variable or the provided path does not exist.
47+
func requireExec(tb testing.TB, name string) {
48+
tb.Helper()
49+
50+
_, err := exec.LookPath(name)
51+
if err != nil {
52+
tb.Skipf("skipping: executable %q not found on PATH", name)
53+
}
54+
}
55+
56+
// integrationSystemLogger returns a slog.Logger configured for system logging
57+
// in integration tests.
58+
func integrationSystemLogger(tb testing.TB) (l *slog.Logger) {
59+
tb.Helper()
60+
61+
ctx := testutil.ContextWithTimeout(tb, testTimeout)
62+
sl, err := agdcslog.NewSystemLogger(ctx, testServiceName)
63+
require.NoError(tb, err)
64+
65+
testutil.CleanupAndRequireSuccess(tb, sl.Close)
66+
67+
h := agdcslog.NewSyslogHandler(sl, &slog.HandlerOptions{
68+
Level: slog.LevelDebug,
69+
})
70+
71+
return slog.New(h)
72+
}
73+
74+
// requireEventuallyFound writes a message to the system log and observes it
75+
// using the provided finder function.
76+
func requireEventuallyFound(
77+
tb testing.TB,
78+
cmdLogReader string,
79+
find func(ctx context.Context, msg string) (ok bool, err error),
80+
) {
81+
tb.Helper()
82+
83+
requireIntegration(tb)
84+
requireExec(tb, cmdLogReader)
85+
86+
l := integrationSystemLogger(tb)
87+
msg := strconv.FormatInt(time.Now().UnixNano(), 10)
88+
89+
ctx := testutil.ContextWithTimeout(tb, testTimeout)
90+
l.InfoContext(ctx, msg)
91+
92+
require.EventuallyWithT(tb, func(ct *assert.CollectT) {
93+
findCtx, cancel := context.WithTimeout(ctx, testTimeout)
94+
defer cancel()
95+
96+
ok, err := find(findCtx, msg)
97+
require.NoError(ct, err)
98+
99+
assert.True(ct, ok)
100+
}, testTimeout, testTimeout/10)
101+
}
102+
18103
// testLogger is a mock implementation of [agdcslog.SystemLogger] interface for
19104
// tests.
20105
//
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//go:build darwin
2+
3+
package agdcslog_test
4+
5+
import (
6+
"bytes"
7+
"context"
8+
"fmt"
9+
"strings"
10+
"testing"
11+
12+
"github.com/AdguardTeam/golibs/osutil/executil"
13+
)
14+
15+
// cmdLogReader is the name of the system log reader.
16+
const cmdLogReader = "log"
17+
18+
// findInLog searches the macOS unified log for the message.
19+
func findInLog(ctx context.Context, msg string) (ok bool, err error) {
20+
var stdOut, stdErr bytes.Buffer
21+
22+
err = executil.Run(ctx, executil.SystemCommandConstructor{}, &executil.CommandConfig{
23+
Path: cmdLogReader,
24+
Args: []string{"show", "--style", "syslog", "--last", testTimeout.String(), "--info"},
25+
Stdout: &stdOut,
26+
Stderr: &stdErr,
27+
})
28+
if err != nil {
29+
return false, fmt.Errorf("log search failed: %w; stderr=%q", err, &stdErr)
30+
}
31+
32+
return strings.Contains(stdOut.String(), msg), nil
33+
}
34+
35+
func TestSystemLogger_integration(t *testing.T) {
36+
requireEventuallyFound(t, cmdLogReader, findInLog)
37+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//go:build linux
2+
3+
package agdcslog_test
4+
5+
import (
6+
"bytes"
7+
"context"
8+
"fmt"
9+
"strings"
10+
"testing"
11+
12+
"github.com/AdguardTeam/golibs/osutil/executil"
13+
)
14+
15+
const (
16+
// systemdLogReader is the systemd log reader.
17+
systemdLogReader = "journalctl"
18+
19+
// syslogLogReader is the BusyBox syslog log reader.
20+
syslogLogReader = "logread"
21+
)
22+
23+
// findWithJournalctl searches the systemd journal for the message.
24+
func findWithJournalctl(ctx context.Context, msg string) (ok bool, err error) {
25+
var stdOut, stdErr bytes.Buffer
26+
27+
since := "-" + testTimeout.String()
28+
29+
err = executil.Run(ctx, executil.SystemCommandConstructor{}, &executil.CommandConfig{
30+
Path: systemdLogReader,
31+
Args: []string{"-o", "cat", "-t", testServiceName, "--since", since, "--no-pager"},
32+
Stdout: &stdOut,
33+
Stderr: &stdErr,
34+
})
35+
if err != nil {
36+
return false, fmt.Errorf("log search failed: %w; stderr=%q", err, &stdErr)
37+
}
38+
39+
return strings.Contains(stdOut.String(), msg), nil
40+
}
41+
42+
// findWithLogread searches the syslog ring buffer for the message.
43+
func findWithLogread(ctx context.Context, msg string) (ok bool, err error) {
44+
var stdOut, stdErr bytes.Buffer
45+
46+
err = executil.Run(ctx, executil.SystemCommandConstructor{}, &executil.CommandConfig{
47+
Path: syslogLogReader,
48+
Args: nil,
49+
Stdout: &stdOut,
50+
Stderr: &stdErr,
51+
})
52+
if err != nil {
53+
return false, fmt.Errorf("log search failed: %w; stderr=%q", err, &stdErr)
54+
}
55+
56+
return strings.Contains(stdOut.String(), msg), nil
57+
}
58+
59+
func TestSystemLogger_integration_systemd(t *testing.T) {
60+
requireEventuallyFound(t, systemdLogReader, findWithJournalctl)
61+
}
62+
63+
func TestSystemLogger_integration_syslogd(t *testing.T) {
64+
requireEventuallyFound(t, syslogLogReader, findWithLogread)
65+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//go:build windows
2+
3+
package agdcslog_test
4+
5+
import (
6+
"bytes"
7+
"context"
8+
"fmt"
9+
"testing"
10+
11+
"github.com/AdguardTeam/golibs/osutil/executil"
12+
)
13+
14+
// cmdLogReader is the name of the system log reader.
15+
const cmdLogReader = "wevtutil"
16+
17+
// findInLog searches the Windows Application event log for the message.
18+
func findInLog(ctx context.Context, msg string) (ok bool, err error) {
19+
var stdOut, stdErr bytes.Buffer
20+
21+
ms := testTimeout.Milliseconds()
22+
query := fmt.Sprintf(`*[System[TimeCreated[timediff(@SystemTime) <= %d]]]`, ms)
23+
24+
err = executil.Run(ctx, executil.SystemCommandConstructor{}, &executil.CommandConfig{
25+
Path: cmdLogReader,
26+
Args: []string{"qe", "Application", "/rd:true", "/f:text", "/uni", "/q:" + query},
27+
Stdout: &stdOut,
28+
Stderr: &stdErr,
29+
})
30+
if err != nil {
31+
return false, fmt.Errorf("log search failed: %w; stderr=%q", err, &stdErr)
32+
}
33+
34+
// Strip NUL bytes from UTF-16LE output.
35+
out := stdOut.Bytes()
36+
out = bytes.ReplaceAll(out, []byte{0x00}, nil)
37+
38+
return bytes.Contains(out, []byte(msg)), nil
39+
}
40+
41+
func TestSystemLogger_integration(t *testing.T) {
42+
requireEventuallyFound(t, cmdLogReader, findInLog)
43+
}

0 commit comments

Comments
 (0)