Skip to content

Commit 68dba0e

Browse files
authored
Deprecating escript: Introducing golang only tests (lf-edge#1037)
Instead of using escript to write tests, one now can write tests fully in golang and run those like this ``` cd tests/sec go test ``` So there's no layered complexity anymore, compared to calling calling forked from golang test binary, which is written by us which actually interprets text files as bash commands, execs those via exec and sometimes substitute some configuration with that's been written in specification below or call arbitratry bash scripts, or compare values from stdout and file using one character command ! For more information refer to lf-edge#1037 Signed-off-by: Pavel Abramov <[email protected]>
1 parent 0c8f36f commit 68dba0e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+1123
-510
lines changed

.github/actions/setup-environment/action.yml

+8
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ runs:
6565
./eden config set default --key=eve.accel --value=false
6666
./eden config set default --key=eve.firmware --value="$(pwd)/firmware/OVMF_CODE.fd $(pwd)/firmware/OVMF_VARS.fd"
6767
fi
68+
./dist/bin/eden+ports.sh 2223:2223 2224:2224 5912:5902 5911:5901 8027:8027 8028:8028 8029:8029 8030:8030 8031:8031
6869
./eden config set default --key=eve.tpm --value=${{ inputs.tpm_enabled }}
6970
./eden config set default --key=eve.cpu --value=2
7071
shell: bash
@@ -116,3 +117,10 @@ runs:
116117
./eden setup -v debug --grub-options='set_global dom0_extra_args "$dom0_extra_args eve_install_zfs_with_raid_level "'
117118
shell: bash
118119
working-directory: "./eden"
120+
121+
- name: Start and Onboard
122+
run: |
123+
./eden start -v debug
124+
./eden eve onboard -v debug
125+
shell: bash
126+
working-directory: "./eden"

cmd/certs.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func newCertsCmd(cfg *openevec.EdenSetupArgs) *cobra.Command {
2020
Short: "manage certs",
2121
Long: `Managed certificates for Adam and EVE.`,
2222
Run: func(cmd *cobra.Command, args []string) {
23-
if err := eden.GenerateEveCerts(cfg.Eden.CertsDir, cfg.Adam.CertsDomain, cfg.Adam.CertsIP, cfg.Adam.CertsEVEIP, cfg.Eve.CertsUUID, cfg.Eve.DevModel, cfg.Eve.Ssid, cfg.Eve.Password, grubOptions, cfg.Adam.APIv1); err != nil {
23+
if err := eden.GenerateEveCerts(cfg.Eden.CertsDir, cfg.Adam.CertsDomain, cfg.Adam.CertsIP, cfg.Adam.CertsEVEIP, cfg.Eve.CertsUUID, cfg.Eve.DevModel, cfg.Eve.Ssid, cfg.Eve.Arch, cfg.Eve.Password, grubOptions, cfg.Adam.APIv1); err != nil {
2424
log.Errorf("cannot GenerateEveCerts: %s", err)
2525
} else {
2626
log.Info("GenerateEveCerts done")

cmd/edenConfig.go

+14-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cmd
22

33
import (
44
"fmt"
5+
"os"
56

67
"github.com/lf-edge/eden/pkg/defaults"
78
"github.com/lf-edge/eden/pkg/openevec"
@@ -10,7 +11,14 @@ import (
1011
)
1112

1213
func newConfigCmd(configName, verbosity *string) *cobra.Command {
13-
cfg := &openevec.EdenSetupArgs{}
14+
currentPath, err := os.Getwd()
15+
if err != nil {
16+
log.Fatal(err)
17+
}
18+
cfg, err := openevec.GetDefaultConfig(currentPath)
19+
if err != nil {
20+
log.Fatalf("Failed to generate default config %v\n", err)
21+
}
1422
var configCmd = &cobra.Command{
1523
Use: "config",
1624
Short: "work with config",
@@ -73,13 +81,13 @@ func newConfigAddCmd(cfg *openevec.EdenSetupArgs) *cobra.Command {
7381
configAddCmd.Flags().StringVarP(&cfg.Eve.QemuFileToSave, "qemu-config", "", defaults.DefaultQemuFileToSave, "file to save config")
7482
configAddCmd.Flags().IntVarP(&cfg.Eve.QemuCpus, "cpus", "", defaults.DefaultCpus, "cpus")
7583
configAddCmd.Flags().IntVarP(&cfg.Eve.QemuMemory, "memory", "", defaults.DefaultMemory, "memory (MB)")
76-
configAddCmd.Flags().StringSliceVarP(&cfg.Eve.QemuFirmware, "eve-firmware", "", nil, "firmware path")
77-
configAddCmd.Flags().StringVarP(&cfg.Eve.QemuConfigPath, "config-part", "", "", "path for config drive")
78-
configAddCmd.Flags().StringVarP(&cfg.Eve.QemuDTBPath, "dtb-part", "", "", "path for device tree drive (for arm)")
84+
configAddCmd.Flags().StringSliceVarP(&cfg.Eve.QemuFirmware, "eve-firmware", "", cfg.Eve.QemuFirmware, "firmware path")
85+
configAddCmd.Flags().StringVarP(&cfg.Eve.QemuConfigPath, "config-part", "", cfg.Eve.QemuConfigPath, "path for config drive")
86+
configAddCmd.Flags().StringVarP(&cfg.Eve.QemuDTBPath, "dtb-part", "", cfg.Eve.QemuDTBPath, "path for device tree drive (for arm)")
7987
configAddCmd.Flags().StringToStringVarP(&cfg.Eve.HostFwd, "eve-hostfwd", "", defaults.DefaultQemuHostFwd, "port forward map")
8088
configAddCmd.Flags().StringVar(&cfg.Eve.Ssid, "ssid", "", "set ssid of wifi for rpi")
81-
configAddCmd.Flags().StringVar(&cfg.Eve.Arch, "arch", "", "arch of EVE (amd64 or arm64)")
82-
configAddCmd.Flags().StringVar(&cfg.Eve.ModelFile, "devmodel-file", "", "File to use for overwrite of model defaults")
89+
configAddCmd.Flags().StringVar(&cfg.Eve.Arch, "arch", cfg.Eve.Arch, "arch of EVE (amd64 or arm64)")
90+
configAddCmd.Flags().StringVar(&cfg.Eve.ModelFile, "devmodel-file", cfg.Eve.ModelFile, "File to use for overwrite of model defaults")
8391
configAddCmd.Flags().BoolVarP(&force, "force", "", false, "force overwrite config file")
8492

8593
return configAddCmd

cmd/edenSetup.go

-3
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,6 @@ func newSetupCmd(configName, verbosity *string) *cobra.Command {
2323
Long: `Setup harness.`,
2424
PersistentPreRunE: preRunViperLoadFunction(cfg, configName, verbosity),
2525
Run: func(cmd *cobra.Command, args []string) {
26-
if err := openevec.ConfigCheck(*configName); err != nil {
27-
log.Fatalf("Config check failed %s", err)
28-
}
2926
if err := openEVEC.SetupEden(*configName, configDir, softSerial, zedControlURL, ipxeOverride, grubOptions, netboot, installer); err != nil {
3027

3128
log.Fatalf("Setup eden failed: %s", err)

cmd/edenStop.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func newStopCmd(configName, verbosity *string) *cobra.Command {
2727
registryRm, eServerRm,
2828
cfg.Eve.Remote, cfg.Eve.Pid,
2929
swtpmPidFile(cfg), cfg.Sdn.PidFile,
30-
cfg.Eve.DevModel, vmName,
30+
cfg.Eve.DevModel, vmName, cfg.Sdn.Disable,
3131
)
3232
},
3333
}

cmd/edenTest.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66

77
"github.com/lf-edge/eden/pkg/defaults"
88
"github.com/lf-edge/eden/pkg/openevec"
9-
"github.com/lf-edge/eden/pkg/utils"
109
log "github.com/sirupsen/logrus"
1110
"github.com/spf13/cobra"
1211
)
@@ -42,7 +41,7 @@ test <test_dir> -r <regexp> [-t <timewait>] [-v <level>]
4241
}
4342
}
4443

45-
vars, err := utils.InitVars()
44+
vars, err := openevec.InitVarsFromConfig(cfg)
4645

4746
if err != nil {
4847
return fmt.Errorf("error reading config: %s\n", err)

docs/design-decisions.md

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Design decisions
2+
3+
This document is a collection of high-level decisions, that have been made in the project. Why do we have a section about how things use to be? In case if we want to revisit certain concept it will be useful to know why we chose one approach over another.
4+
5+
## Using Domain Specific Language (DSL) vs writing native golang tests
6+
7+
When talking about Eden tests till version 0.9.12, we are talking about `escript`, a Domain Specific Language (DSL) which describes test case and uses Eden to setup, environment. Escript looks like this
8+
9+
```bash
10+
# 1 Setup environment variables
11+
{{$port := "2223"}}
12+
{{$network_name := "n1"}}
13+
{{$app_name := "eclient"}}
14+
15+
# ...
16+
17+
# 2 run eden commands
18+
eden -t 1m network create 10.11.12.0/24 -n {{$network_name}}
19+
20+
# 3 run escript commands
21+
test eden.network.test -test.v -timewait 10m ACTIVATED {{$network_name}}
22+
23+
eden pod deploy -n {{$app_name}} --memory=512MB {{template "eclient_image"}} -p {{$port}}:22 --networks={{$network_name}}
24+
25+
# 4 execute shell script which are defined inside escript file
26+
exec -t 5m bash ssh.sh
27+
stdout 'Ubuntu'
28+
29+
# 5 overwrite configuration
30+
-- eden-config.yml --
31+
{{/* Test's config. file */}}
32+
test:
33+
controller: adam://{{EdenConfig "adam.ip"}}:{{EdenConfig "adam.port"}}
34+
eve:
35+
{{EdenConfig "eve.name"}}:
36+
onboard-cert: {{EdenConfigPath "eve.cert"}}
37+
serial: "{{EdenConfig "eve.serial"}}"
38+
model: {{EdenConfig "eve.devmodel"}}
39+
40+
-- ssh.sh --
41+
EDEN={{EdenConfig "eden.root"}}/{{EdenConfig "eden.bin-dist"}}/{{EdenConfig "eden.eden-bin"}}
42+
for i in `seq 20`
43+
do
44+
sleep 20
45+
# Test SSH-access to container
46+
echo $i\) $EDEN sdn fwd eth0 {{$port}} -- {{template "ssh"}} grep Ubuntu /etc/issue
47+
$EDEN sdn fwd eth0 {{$port}} -- {{template "ssh"}} grep Ubuntu /etc/issue && break
48+
done
49+
```
50+
51+
So you can
52+
53+
1) setup some environment variables
54+
2) run eden commands
55+
3) run escript commands with test
56+
4) execute user-defined shell scripts
57+
5) overwrite eden configuration
58+
59+
Escript file is fed as input to eden test command, which parses the variables using golang templates to substitute some variables using templates in golang, then it's going to read line by line and execute it via os.exec call in golang. test is actually a compiled golang binary, we compile it inside eden repo and then execute it, under the hood it is a manually forked version of standard golang test package with some added commands.
60+
61+
For more information on specific topics refer to following documents:
62+
63+
- Escript test structure [doc](./escript/test-anatomy-sample.md)
64+
- Writing eden tasks for escript [doc](./escript/task-writing.md)
65+
- Running tests [doc](./escript/test-running.md)
66+
67+
So there are several problems with that approach:
68+
69+
**You have to be an escript expert**: when new person comes to a project, they will have some industry skills, like C++ expertise, Rust, Golang, Computer Networking, etc. but if you never worked on this project, you never heard about escript, its' sole purpose was to be part of Eden and be useful language to describe tests for Eden. One might argue, but that's just bash on steroids. Well, true, but do you know what `!` means? It's not equal parameter in escript. So it is like bash, but not exactly it, and it might take time to figure out other hidden features, that could be solved by proper documentation. But before spiraling down that conversation lets think about tools? Imagine, that you wrote new fancy eden test and it doesn't work. Usual thing, you would say. Test Driven Development is all about writing failing tests first and then making them work.
70+
71+
**But how do you debug that test?** Because you're running an interpreter, which runs bash commands via os.exec plus you execute golang program, which is written and compiled inside the repository. Debug printing would work, but I find debuggers much more useful and efficient most of the times. It is a matter of taste, but better to debugger at your disposal, than not to have it in this case. Even worse, how do you debug escript problem? You need to know it's internals, there's no Stackoverflow or Google by your side, nor there are books about it.
72+
73+
**Bumping golang is actually manual action**: last but not least, we have to maintain custom test files which a basically copy-paste with one added function, bumping golang turns into some weird dances in that case.
File renamed without changes.
File renamed without changes.
File renamed without changes.

docs/writing-tests.md

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# How to write tests with Eden
2+
3+
The eden golang test SDK for eve consists of two categories of functions:
4+
5+
1. `openevec`: infrastructure manager. `openevec` enables you to deploy a controller, a backend for that controller, deploy an edge node, etc. It does not change anything on an individual edge node.
6+
2. `evetestkit`: provides useful collection of functions to describe expected state of the system (controller, EVE, AppInstances)
7+
8+
And those functions are used in context of standard golang test library. We do a setup in TestMain (for more info check [this](https://pkg.go.dev/testing#hdr-Main)) and then write test functions which interact with the environment we created. Setting up environment takes couple of minutes, so it makes sense to do it once and run tests within that environment.
9+
10+
Source for the example below can be found [here](../tests/sec/sec_test.go)
11+
12+
In order to test a feature in Eden you need to
13+
14+
## 1. Create a configuration file and describe your environment
15+
16+
The glue between `openevec` and `evetestkit` is a configuration structure called `EdenSetupArgs`. It contains all the necessary information to setup all the components of the test: controller, EVE, etc. You can fill in the structure manually, however `openevec` provides you with a convenient function to create default configuration structure providing project root path:
17+
18+
```go
19+
// ...
20+
func TestMain(m *testing.M) {
21+
currentPath, err := os.Getwd()
22+
if err != nil {
23+
log.Fatal(err)
24+
}
25+
twoLevelsUp := filepath.Dir(filepath.Dir(currentPath))
26+
27+
cfg := openevec.GetDefaultConfig(twoLevelsUp)
28+
29+
if err = openevec.ConfigAdd(cfg, cfg.ConfigName, "", false); err != nil {
30+
log.Fatal(err)
31+
}
32+
// ...
33+
}
34+
```
35+
36+
Due to backward compatibility with escript as of time of writing, the project root path should be Eden repository folder. So if you add tests in the `eden/tests/my-awesome-test` you need to go two levels up, will be removed with escript
37+
Also we need to write configuration file to file system, because there are components (like changer) which read configuration from file system, will be removed with escript
38+
39+
## 2. Initialize `openevec` Setup, Start and Onboard EVE node
40+
41+
When configuration you need `openevec` to create all needed certificates, start backend. For that we create `openevec` object based on a configuration provided, it is just a convinient wrapper.
42+
43+
```go
44+
// ...
45+
func TestMain(m *testing.M) {
46+
// ...
47+
evec := openvec.CreateOpenEVEC(cfg)
48+
49+
evec.SetupEden(/* ... */)
50+
evec.StartEden(/* ... */)
51+
evec.OnboardEve(/* ... */)
52+
53+
// ...
54+
}
55+
```
56+
57+
## 3. Initialize `evetestkit` and run test suite
58+
59+
`evetestkit` provides an abstraction over EveNode, which is used to describe expected state of the system. Each EveNode is running within a project You can create a global object within one test file to use it across multiple tests. Note that EveNode is not threadsafe, since controller is stateful, so tests should be run consequently (no t.Parallel())
60+
61+
```go
62+
const projectName = "security-test"
63+
var eveNode *evetestkit.EveNode
64+
65+
func TestMain(m *testing.M) {
66+
// ...
67+
node, err := evetestkit.InitializeTestFromConfig(projectName, cfg, evetestkit.WithControllerVerbosity("debug"))
68+
if err != nil {
69+
log.Fatalf("Failed to initialize test: %v", err)
70+
}
71+
72+
eveNode = node
73+
res := m.Run()
74+
os.Exit(res)
75+
}
76+
```
77+
78+
## 4. Write your test
79+
80+
Below is an example of test, which check if AppArmor is enabled (specific file on EVE exists). It uses `EveReadFile` function from `evetestkit`
81+
82+
```go
83+
const appArmorStatus = "/sys/module/apparmor/parameters/enabled"
84+
// ...
85+
func TestAppArmorEnabled(t *testing.T) {
86+
log.Println("TestAppArmorEnabled started")
87+
defer log.Println("TestAppArmorEnabled finished")
88+
89+
out, err := eveNode.EveReadFile(appArmorStatus)
90+
if err != nil {
91+
t.Fatal(err)
92+
}
93+
94+
exits := strings.TrimSpace(string(out))
95+
if exits != "Y" {
96+
t.Fatal("AppArmor is not enabled")
97+
}
98+
```

0 commit comments

Comments
 (0)