Skip to content

Commit bd294f5

Browse files
authored
internal/testrunner/script: add script testing package (#3012)
This adds script testing for data streams. This is the MVP, future versions can be generalised to operate on input packages. The current implementation supports: * pipeline testing * system testing * package upgrade testing (only to latest, not to arbitrary versions) * shared stack * independent stack * docker services Not supported: * test coverage * report output configuration * k8s services * tf services (ish, these can be shimmed via docker)
1 parent d38ab6c commit bd294f5

34 files changed

+3470
-1
lines changed

.buildkite/pipeline.trigger.integration.tests.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ CHECK_PACKAGES_TESTS=(
4545
test-check-packages-with-kind
4646
test-check-packages-with-custom-agent
4747
test-check-packages-benchmarks
48+
test-check-packages-independent-script
4849
)
4950
for test in "${CHECK_PACKAGES_TESTS[@]}"; do
5051
test_name=${test#"test-check-packages-"}

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ test-check-packages-with-kind:
100100
test-check-packages-other:
101101
PACKAGE_TEST_TYPE=other ./scripts/test-check-packages.sh
102102

103+
test-check-packages-independent-script:
104+
elastic-package test script -C test/packages/other/with_script --external-stack=false --defer-cleanup 1s
105+
103106
test-check-packages-false-positives:
104107
PACKAGE_TEST_TYPE=false_positives ./scripts/test-check-false-positives.sh
105108

@@ -136,7 +139,7 @@ test-profiles-command:
136139
test-check-update-version:
137140
./scripts/test-check-update-version.sh
138141

139-
test: test-go test-stack-command test-check-packages test-profiles-command test-build-install-zip test-build-zip test-build-install-zip-file test-build-install-zip-file-shellinit test-check-update-version test-profiles-command test-system-test-flags
142+
test: test-go test-stack-command test-check-packages test-check-packages-independent-script test-profiles-command test-build-install-zip test-build-zip test-build-install-zip-file test-build-install-zip-file-shellinit test-check-update-version test-profiles-command test-system-test-flags
140143

141144
check-git-clean:
142145
git update-index --really-refresh

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,12 @@ _Context: package_
628628

629629
Run policy tests for the package.
630630

631+
### `elastic-package test script`
632+
633+
_Context: package_
634+
635+
Run script tests for the package.
636+
631637
### `elastic-package test static`
632638

633639
_Context: package_

cmd/testrunner.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/elastic/elastic-package/internal/testrunner/runners/policy"
3232
"github.com/elastic/elastic-package/internal/testrunner/runners/static"
3333
"github.com/elastic/elastic-package/internal/testrunner/runners/system"
34+
"github.com/elastic/elastic-package/internal/testrunner/script"
3435
)
3536

3637
const testLongDescription = `Use this command to run tests on a package. Currently, the following types of tests are available:
@@ -95,6 +96,9 @@ func setupTestCommand() *cobraext.Command {
9596
systemCmd := getTestRunnerSystemCommand()
9697
cmd.AddCommand(systemCmd)
9798

99+
scriptCmd := getTestRunnerScriptCommand()
100+
cmd.AddCommand(scriptCmd)
101+
98102
policyCmd := getTestRunnerPolicyCommand()
99103
cmd.AddCommand(policyCmd)
100104

@@ -607,6 +611,46 @@ func testRunnerSystemCommandAction(cmd *cobra.Command, args []string) error {
607611
return nil
608612
}
609613

614+
func getTestRunnerScriptCommand() *cobra.Command {
615+
cmd := &cobra.Command{
616+
Use: "script",
617+
Short: "Run script tests",
618+
Long: "Run script tests for the package.",
619+
Args: cobra.NoArgs,
620+
RunE: testRunnerScriptCommandAction,
621+
}
622+
623+
cmd.Flags().String(cobraext.ScriptsFlagName, "", cobraext.ScriptsFlagDescription)
624+
cmd.Flags().Bool(cobraext.ExternalStackFlagName, true, cobraext.ExternalStackFlagDescription)
625+
cmd.Flags().StringSliceP(cobraext.DataStreamsFlagName, "d", nil, cobraext.DataStreamsFlagDescription)
626+
cmd.Flags().String(cobraext.RunPatternFlagName, "", cobraext.RunPatternFlagDescription)
627+
cmd.Flags().BoolP(cobraext.UpdateScriptTestArchiveFlagName, "u", false, cobraext.UpdateScriptTestArchiveFlagDescription)
628+
cmd.Flags().BoolP(cobraext.WorkScriptTestFlagName, "w", false, cobraext.WorkScriptTestFlagDescription)
629+
cmd.Flags().Bool(cobraext.ContinueOnErrorFlagName, false, cobraext.ContinueOnErrorFlagDescription)
630+
cmd.Flags().Bool(cobraext.VerboseScriptFlagName, false, cobraext.VerboseScriptFlagDescription)
631+
632+
cmd.MarkFlagsMutuallyExclusive(cobraext.ScriptsFlagName, cobraext.DataStreamsFlagName)
633+
634+
return cmd
635+
}
636+
637+
func testRunnerScriptCommandAction(cmd *cobra.Command, args []string) error {
638+
cmd.Println("Run script tests for the package")
639+
pkgRoot, err := packages.FindPackageRoot()
640+
if err != nil {
641+
if err == packages.ErrPackageRootNotFound {
642+
return errors.New("package root not found")
643+
}
644+
return fmt.Errorf("locating package root failed: %w", err)
645+
}
646+
pkg := filepath.Base(pkgRoot)
647+
cmd.Printf("--- Test results for package: %s - START ---\n", pkg)
648+
err = script.Run(cmd.OutOrStderr(), cmd, args)
649+
cmd.Printf("--- Test results for package: %s - END ---\n", pkg)
650+
cmd.Println("Done")
651+
return err
652+
}
653+
610654
func getTestRunnerPolicyCommand() *cobra.Command {
611655
cmd := &cobra.Command{
612656
Use: "policy",

docs/howto/script_testing.md

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# HOWTO: Writing script tests for a package
2+
3+
Script testing is an advanced topic that assumes knowledge of [pipeline](./pipeline_testing.md)
4+
and [system](./system_testing.md) testing.
5+
6+
Testing packages with script testing is only intended for testing cases that
7+
cannot be adequately covered by the pipeline and system testing tools such as
8+
testing failure paths and package upgrades. It can also be used for debugging
9+
integrations stack issues.
10+
11+
## Introduction
12+
13+
The script testing system is built on the Go testscript package with extensions
14+
provided to allow scripting of stack and integration operations such as
15+
bringing up a stack, installing packages and running agents. For example, using
16+
these commands it is possible to express a system test as described in the
17+
system testing [Conceptual Process](./system_testing.md#conceptual-process) section.
18+
19+
20+
## Expressing tests
21+
22+
Tests are written as [txtar format](https://pkg.go.dev/golang.org/x/tools/txtar#hdr-Txtar_format)
23+
files in a data stream's \_dev/test/scripts directory. The logic for the test is
24+
written in the txtar file's initial comment section and any additional resource
25+
files are included in the txtar file's files sections.
26+
27+
The standard commands and behaviors for testscript scripts are documented in
28+
the [testscript package documentation](https://pkg.go.dev/github.com/rogpeppe/go-internal/testscript).
29+
30+
31+
## Extension commands
32+
33+
The test script command provides additional commands to aid in interacting with
34+
a stack, starting agents and services and validating results.
35+
36+
- `sleep <duration>`: sleep for a duration (Go `time.Duration` parse syntax)
37+
- `date [<ENV_VAR_NAME>]`: print the current time in RFC3339, optionally setting a variable with the value
38+
- `GET [-json] <url>`: perform an HTTP GET request, emitting the response body to stdout and optionally formatting indented JSON
39+
- `POST [-json] [-content <content-type>] <body-path> <url>`: perform an HTTP POST request, emitting the response body to stdout and optionally formatting indented JSON
40+
- `match_file <pattern_file_path> <data_path>`: perform a grep pattern match between a pattern file and a data file
41+
42+
- stack commands:
43+
- `stack_up [-profile <profile>] [-provider <provider>] [-timeout <duration>] <stack-version>`: bring up a version of the Elastic stack
44+
- `use_stack [-profile <profile>] [-timeout <duration>]`: use a running Elastic stack
45+
- `stack_down [-profile <profile>] [-provider <provider>] [-timeout <duration>]`: take down a started Elastic stack
46+
- `dump_logs [-profile <profile>] [-provider <provider>] [-timeout <duration>] [-since <RFC3339 time>] [<dirpath>]`: dump the logs from the stack into a directory, collecting internal logs into an ordered complete set with the base name elastic-agent-all.ndjson
47+
- `get_policy [-profile <profile>] [-timeout <duration>] <policy_name>`: print the details for a policy
48+
49+
- agent commands:
50+
- `install_agent [-profile <profile>] [-timeout <duration>] [<network_name_label>]`: install an Elastic Agent policy, setting the environment variable named in the positional argument
51+
- `uninstall_agent [-profile <profile>] [-timeout <duration>]`: remove an installed Elastic Agent policy
52+
53+
- package commands:
54+
- `add_package [-profile <profile>] [-timeout <duration>]`: add the current package's assets
55+
- `remove_package [-profile <profile>] [-timeout <duration>]`: remove assets for the current package
56+
- `add_package_zip [-profile <profile>] [-timeout <duration>] <path_to_zip>`: add assets from a Zip-packaged integration package
57+
- `remove_package_zip [-profile <profile>] [-timeout <duration>] <path_to_zip>`: remove assets for Zip-packaged integration package
58+
- `upgrade_package_latest [-profile <profile>] [-timeout <duration>] [<package_name>]`: upgrade the current package or another named package to the latest version
59+
- `add_package_policy [-profile <profile>] [-timeout <duration>] [-policy <policy_name>] <config.yaml> <name_var_label>`: add a package policy, setting the environment variable named in the positional argument
60+
- `remove_package_policy [-profile <profile>] [-timeout <duration>] <data_stream_name>`: remove a package policy
61+
- `get_docs [-profile <profile>] [-timeout <duration>] [<data_stream>]`: get documents from a data stream
62+
63+
- docker commands:
64+
- `docker_up [-profile <profile>] [-timeout <duration>] <dir>`: start a docker service defined in the provided directory
65+
- `docker_down [-timeout <duration>] <name>`: stop a started docker service and print the docker logs to stdout
66+
- `docker_signal [-timeout <duration>] <name> <signal>`: send a signal to a running docker service
67+
- `docker_wait_exit [-timeout <duration>] <name>`: wait for a docker service to exit
68+
69+
- pipeline commands:
70+
- `install_pipelines [-profile <profile>] [-timeout <duration>] <path_to_data_stream>`: install ingest pipelines from a path
71+
- `simulate [-profile <profile>] [-timeout <duration>] <path_to_data_stream> <pipeline> <path_to_data>`: run a pipeline test, printing the result as pretty-printed JSON to standard output
72+
- `uninstall_pipelines [-profile <profile>] [-timeout <duration>] <path_to_data_stream>`: remove installed ingest pipelines
73+
74+
75+
## Environment variables
76+
77+
- `PROFILE`: the `elastic-package` profile being used
78+
- `CONFIG_ROOT`: the `elastic-package` configuration root path
79+
- `CONFIG_PROFILES`: the `elastic-package` profiles configuration root path
80+
- `HOME`: the user's home directory path
81+
- `PACKAGE_NAME`: the name of the running package
82+
- `PACKAGE_BASE`: the basename of the path to the root of the running package
83+
- `PACKAGE_ROOT`: the path to the root of the running package
84+
- `CURRENT_VERSION`: the current version of the package
85+
- `PREVIOUS_VERSION`: the previous version of the package
86+
- `DATA_STREAM`: the name of the data stream
87+
- `DATA_STREAM_ROOT`: the path to the root of the data stream
88+
- `WORK`: the path to the directory that the script is run in
89+
90+
91+
## Conditions
92+
93+
The testscript package allows conditions to be set that allow conditional
94+
execution of commands. The test script command adds a condition that reflects
95+
the state of the `--external-stack` flag. This allows tests to be written that
96+
conditionally use either an externally managed stack, or a stack that has been
97+
started by the test script.
98+
99+
100+
## Example
101+
102+
As an example, a basic system test could be expressed as follows.
103+
```
104+
# Only run the test if --external-stack=true.
105+
[!external_stack] skip 'Skipping external stack test.'
106+
# Only run the test if the jq executable is in $PATH. This is needed for a test below.
107+
[!exec:jq] skip 'Skipping test requiring absent jq command'
108+
109+
# Register running stack.
110+
use_stack -profile ${CONFIG_PROFILES}/${PROFILE}
111+
112+
# Install an agent.
113+
install_agent -profile ${CONFIG_PROFILES}/${PROFILE} NETWORK_NAME
114+
115+
# Bring up a docker container.
116+
#
117+
# The service is described in the test-hits/docker-compose.yml below with
118+
# its logs in test-hits/logs/generated.log.
119+
docker_up -profile ${CONFIG_PROFILES}/${PROFILE} -network ${NETWORK_NAME} test-hits
120+
121+
# Add the package resources.
122+
add_package -profile ${CONFIG_PROFILES}/${PROFILE}
123+
124+
# Add the data stream.
125+
#
126+
# The configuration for the test is described in test_config.yaml below.
127+
add_package_policy -profile ${CONFIG_PROFILES}/${PROFILE} test_config.yaml DATA_STREAM_NAME
128+
129+
# Start the service.
130+
docker_signal test-hits SIGHUP
131+
132+
# Wait for the service to exit.
133+
docker_wait_exit -timeout 5m test-hits
134+
135+
# Check that we can see our policy.
136+
get_policy -profile ${CONFIG_PROFILES}/${PROFILE} -timeout 1m ${DATA_STREAM_NAME}
137+
cp stdout got_policy.json
138+
exec jq '.name=="'${DATA_STREAM_NAME}'"' got_policy.json
139+
stdout true
140+
141+
# Take down the service and check logs for our message.
142+
docker_down test-hits
143+
! stderr .
144+
stdout '"total_lines":10'
145+
146+
# Get documents from the data stream.
147+
get_docs -profile ${CONFIG_PROFILES}/${PROFILE} -want 10 -timeout 5m ${DATA_STREAM_NAME}
148+
cp stdout got_docs.json
149+
150+
# Remove the data stream.
151+
remove_package_policy -profile ${CONFIG_PROFILES}/${PROFILE} ${DATA_STREAM_NAME}
152+
153+
# Uninstall the agent.
154+
uninstall_agent -profile ${CONFIG_PROFILES}/${PROFILE} -timeout 1m
155+
156+
# Remove the package resources.
157+
remove_package -profile ${CONFIG_PROFILES}/${PROFILE}
158+
159+
-- test-hits/docker-compose.yml --
160+
version: '2.3'
161+
services:
162+
test-hits:
163+
image: docker.elastic.co/observability/stream:v0.20.0
164+
volumes:
165+
- ./logs:/logs:ro
166+
command: log --start-signal=SIGHUP --delay=5s --addr elastic-agent:9999 -p=tcp /logs/generated.log
167+
-- test-hits/logs/generated.log --
168+
ntpd[1001]: kernel time sync enabled utl
169+
restorecond: : Reset file context quasiarc: liqua
170+
auditd[5699]: Audit daemon rotating log files
171+
anacron[5066]: Normal exit ehend
172+
restorecond: : Reset file context vol: luptat
173+
heartbeat: : <<eumiu.medium> Processing command: accept
174+
restorecond: : Reset file context nci: ofdeFin
175+
auditd[6668]: Audit daemon rotating log files
176+
anacron[1613]: Normal exit mvolu
177+
ntpd[2959]: ntpd gelit-r tatno
178+
-- test_config.yaml --
179+
input: tcp
180+
vars: ~
181+
data_stream:
182+
vars:
183+
tcp_host: 0.0.0.0
184+
tcp_port: 9999
185+
```
186+
187+
Other complete examples can be found in the [with_script test package](https://github.com/elastic/elastic-package/blob/main/test/packages/other/with_script/data_stream/first/_dev/test/scripts).
188+
189+
190+
## Running script tests
191+
192+
The `elastic-package test script` command has the following sub-command-specific
193+
flags:
194+
195+
- `--continue`: continue running the script if an error occurs
196+
- `--data-streams`: comma-separated data streams to test
197+
- `--external-stack`: use external stack for script tests (default true)
198+
- `--run`: run only tests matching the regular expression
199+
- `--scripts`: path to directory containing test scripts (advanced use only)
200+
- `--update`: update archive file if a cmp fails
201+
- `--verbose-scripts`: verbose script test output (show all script logging)
202+
- `--work`: print temporary work directory and do not remove when done
203+
204+
205+
## Limitations
206+
207+
While the testscript package allows reference to paths outside the configuration
208+
root and the package's root, the backing `elastic-package` infrastructure does
209+
not, so it is advised that tests only refer to paths within the `$WORK` and
210+
`$PACKAGE_ROOT` directories.

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ require (
3131
github.com/maxmind/mmdbwriter v1.1.0
3232
github.com/olekukonko/tablewriter v1.1.1
3333
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
34+
github.com/rogpeppe/go-internal v1.13.1
3435
github.com/shirou/gopsutil/v3 v3.24.5
3536
github.com/spf13/cobra v1.10.1
3637
github.com/stretchr/testify v1.11.1

internal/cobraext/flags.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ const (
112112
CheckConditionFlagName = "check-condition"
113113
CheckConditionFlagDescription = "check if the condition is met for the package, but don't install the package (e.g. kibana.version=7.10.0)"
114114

115+
ContinueOnErrorFlagName = "continue"
116+
ContinueOnErrorFlagDescription = "continue running the script if an error occurs"
117+
115118
DaemonModeFlagName = "daemon"
116119
DaemonModeFlagDescription = "daemon mode"
117120

@@ -130,6 +133,9 @@ const (
130133
DumpOutputFlagName = "output"
131134
DumpOutputFlagDescription = "path to directory where exported assets will be stored"
132135

136+
ExternalStackFlagName = "external-stack"
137+
ExternalStackFlagDescription = "use external stack for script tests"
138+
133139
FailOnMissingFlagName = "fail-on-missing"
134140
FailOnMissingFlagDescription = "fail if tests are missing"
135141

@@ -165,6 +171,12 @@ const (
165171
ReportOutputPathFlagName = "report-output-path"
166172
ReportOutputPathFlagDescription = "output path for test report (defaults to %q in build directory)"
167173

174+
RunPatternFlagName = "run"
175+
RunPatternFlagDescription = "run only tests matching the regular expression"
176+
177+
ScriptsFlagName = "scripts"
178+
ScriptsFlagDescription = "path to directory containing test scripts"
179+
168180
ShowAllFlagName = "all"
169181
ShowAllFlagDescription = "show all deployed package revisions"
170182

@@ -224,6 +236,15 @@ const (
224236
NoProvisionFlagName = "no-provision"
225237
NoProvisionFlagDescription = "trigger just system tests wihout setup nor teardown"
226238

239+
UpdateScriptTestArchiveFlagName = "update"
240+
UpdateScriptTestArchiveFlagDescription = "update archive file if a cmp fails"
241+
242+
VerboseScriptFlagName = "verbose-scripts"
243+
VerboseScriptFlagDescription = "verbose script test output"
244+
245+
WorkScriptTestFlagName = "work"
246+
WorkScriptTestFlagDescription = "print temporary work directory and do not remove when done"
247+
227248
ZipPackageFilePathFlagName = "zip"
228249
ZipPackageFilePathFlagShorthand = "z"
229250
ZipPackageFilePathFlagDescription = "path to the zip package file (*.zip)"

0 commit comments

Comments
 (0)