Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 79 additions & 8 deletions evetest/Makefile
Original file line number Diff line number Diff line change
@@ -1,15 +1,44 @@
# Copyright (c) 2026 Zededa, Inc.
# SPDX-License-Identifier: Apache-2.0

EVETEST_VERSION := $(shell grep -v '^\#' VERSION | head -n1)
# \# is make's escape for a literal '#'; expand it via a variable so the shell
# does not receive the backslash (newer grep warns about stray '\' before '#').
HASH := \#
EVETEST_VERSION := $(shell grep -v '^$(HASH)' VERSION | head -n1)
EVETEST_ORG ?= lfedge

EVETEST_ADAM_VERSION ?= 0.0.81
EVETEST_ADAM_REPO ?= lfedge/adam

# Repository holding built EVE images (mirrors constants.DefaultEVERepo).
EVETEST_EVE_REPO ?= lfedge/eve

# linuxkit comes from the EVE repo's build tools (see ../Makefile and
# ../mk/linuxkit.mk); it is built on demand via the root's "linuxkit" target
# and handed to the sdn sub-make through LINUXKIT, so it does not have to be
# on PATH.
BUILDTOOLS_BIN := $(abspath $(CURDIR)/../build-tools/bin)
LINUXKIT := $(BUILDTOOLS_BIN)/linuxkit

EVETEST_IMAGE := $(EVETEST_ORG)/evetest:$(EVETEST_VERSION)
EVETEST_API_PORT ?= 50021

# Label on the evetest image recording the framework content signature it was
# built from; used by ensure-evetest-image to detect stale images.
FRAMEWORK_SIG_LABEL := org.lfedge.evetest.framework-sig

# Framework paths baked into the evetest image. tests/, netmodels/ and
# matchers/ are bind-mounted into the container at run time and testapps/
# images are built separately, so changes there never require a rebuild.
FRAMEWORK_PATHSPEC := . ':(exclude)tests' ':(exclude)netmodels' ':(exclude)matchers' ':(exclude)testapps'

# Content signature of the framework sources: the last commit touching them
# plus any uncommitted (staged or unstaged) changes. Git-based, so a git
# fsmonitor daemon keeps it fast; untracked files are not considered until
# they are added to git. Constant outside a git checkout.
FRAMEWORK_SIG = $(shell { git log -1 --format=%H -- $(FRAMEWORK_PATHSPEC); \
git diff HEAD -- $(FRAMEWORK_PATHSPEC); } 2>/dev/null | sha256sum | cut -d' ' -f1)

PROTOC_VERSION ?= 31.1
PROTOC_GEN_GO_VERSION ?= 1.36.6
PROTOC_GEN_GO_GRPC_VERSION ?= 1.5.1
Expand Down Expand Up @@ -57,8 +86,32 @@ endif
$(eval EVE_VERSION_ENV :=)
ifndef EVETEST_EVE_VERSION
ifeq ($(strip $(filter-out false False FALSE 0 f F,$(EVETEST_EVE_LIVE_IMAGE))),)
@# Resolve which EVE version to test. On a dirty tree 'make version' embeds
@# the wall-clock minute of *this* invocation, so it almost never matches
@# the timestamp baked into the image tag by an earlier 'make eve' run.
@# In that case fall back to the version recorded by the last completed
@# build (dist/<arch>/current, read via 'make currentversion'), provided it
@# is a dirty build of the same commit and its EVE Docker image exists
@# locally. 'make currentversion' reports the full version including the
@# hypervisor/arch suffix; strip it by anchoring on the dirty timestamp.
$(eval EVETEST_EVE_VERSION := $(strip $(shell \
$(MAKE) -s -C $(REPO_ROOT) version 2>/dev/null \
v="$$($(MAKE) -s -C $(REPO_ROOT) version 2>/dev/null)"; \
if echo "$$v" | grep -q -e '-dirty-' && \
! docker images --format '{{.Tag}}' "$(EVETEST_EVE_REPO)" 2>/dev/null \
| grep -q "^$$v-"; then \
base="$${v%%-dirty-*}-dirty-"; \
cur="$$($(MAKE) -s -C $(REPO_ROOT) currentversion 2>/dev/null \
| sed -E 's/(-dirty-[0-9]{4}-[0-9]{2}-[0-9]{2}\.[0-9]{2}\.[0-9]{2}).*/\1/')"; \
if [ -n "$$cur" ] && [ "$${cur#"$$base"}" != "$$cur" ] && \
docker images --format '{{.Tag}}' "$(EVETEST_EVE_REPO)" 2>/dev/null \
| grep -q "^$$cur-"; then \
echo "NOTE: no local EVE image for dirty version $$v;" \
"falling back to last built version $$cur (from dist/current)." >&2; \
echo "NOTE: run 'make eve' first if EVE sources changed since that build." >&2; \
v="$$cur"; \
fi; \
fi; \
echo "$$v" \
)))
$(eval EVE_VERSION_ENV := -e EVETEST_EVE_VERSION=$(EVETEST_EVE_VERSION))
endif
Expand Down Expand Up @@ -143,6 +196,12 @@ endif
echo ""; \
exit 1; \
fi >&2
@# Colorize framework log output only when stdout is a terminal. The test
@# runs inside the container where stdout is never a TTY (go test pipes it),
@# so the detection must happen here. It also cannot use $(shell [ -t 1 ])
@# because within $(shell) stdout is make's capture pipe, never a terminal;
@# only a recipe shell sees make's real stdout.
[ -t 1 ] && COLOR=true || COLOR=false; \
docker run --rm $(DOCKER_IT) \
--name evetest-$(EVETEST_API_PORT) \
-p $(EVETEST_API_PORT):$(EVETEST_API_PORT) \
Expand All @@ -164,6 +223,7 @@ endif
$(DIST_DIR_MOUNT) \
$(GO_CACHE_MOUNT) \
$(DOCKER_CONFIG_MOUNT) \
-e EVETEST_COLOR_OUTPUT=$$COLOR \
$(ENV_VARS) \
$(EVE_VERSION_ENV) \
$(BROKER_IMAGE_ENV) \
Expand All @@ -175,17 +235,24 @@ endif
-e EVETEST_HOST_GID=$(EVETEST_HOST_GID) \
$(EVETEST_IMAGE)

# Only builds when the image is missing -- a locally built image is never
# rebuilt just because harness source changed, so after editing evetest code
# run `make build-container` explicitly before `make evetest`, or the run
# silently uses the stale, previously-built harness.
# Ensure a usable evetest image exists locally: pull or build when it is
# missing, and rebuild when the framework signature recorded in the image
# label no longer matches the current sources (an image without the label,
# e.g. one predating this check, is always considered stale).
ensure-evetest-image:
@if ! docker image inspect $(EVETEST_IMAGE) >/dev/null 2>&1; then \
echo "Docker image $(EVETEST_IMAGE) not found locally, trying to pull..."; \
if ! docker pull $(EVETEST_IMAGE); then \
echo "Pull failed, building image locally..."; \
$(MAKE) build-container; \
exit 0; \
fi; \
fi; \
if [ "$$(docker image inspect \
-f '{{index .Config.Labels "$(FRAMEWORK_SIG_LABEL)"}}' \
$(EVETEST_IMAGE) 2>/dev/null)" != "$(FRAMEWORK_SIG)" ]; then \
echo "evetest framework sources changed, rebuilding $(EVETEST_IMAGE)..."; \
$(MAKE) build-container; \
fi

# Internal target: invoked by proto inside the builder container.
Expand Down Expand Up @@ -220,6 +287,7 @@ build-container:
docker buildx build \
--$(DOCKER_TARGET) \
--platform $(DOCKER_PLATFORM) \
--label $(FRAMEWORK_SIG_LABEL)=$(FRAMEWORK_SIG) \
--build-arg EVETEST_VERSION=$(EVETEST_VERSION) \
--build-arg EVETEST_ADAM_REPO=$(EVETEST_ADAM_REPO) \
--build-arg EVETEST_ADAM_VERSION=$(EVETEST_ADAM_VERSION) \
Expand Down Expand Up @@ -356,5 +424,8 @@ install-cli:
cd ./cli && go build -ldflags "-X main.version=$(EVETEST_VERSION)" -o $(GOBIN)/evetest
@echo "Installed evetest CLI to $(GOBIN)/evetest"

build-sdn-container:
@$(MAKE) -C sdn build
$(LINUXKIT):
@$(MAKE) -C $(CURDIR)/.. linuxkit

build-sdn-container: $(LINUXKIT)
@$(MAKE) -C sdn LINUXKIT=$(LINUXKIT) build
1 change: 1 addition & 0 deletions evetest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,7 @@ non-default behavior.
| `EVETEST_EVE_FIRMWARE_DIR` | Overrides firmware discovery for a local live image, which otherwise looks for `OVMF*.fd` in `installer/firmware` next to the resolved qcow2 | -- |
| `EVETEST_PREFERRED_ARCH` | Preferred CPU architecture (`amd64`, `arm64`) | `amd64` |
| `EVETEST_LOG_LEVEL` | Framework log level (`debug`, `info`, `warn`) | `info` |
| `EVETEST_COLOR_OUTPUT` | Colorize framework log output with ANSI escape codes (`true`/`false`) | auto: enabled only when stdout is a terminal, disabled when piped or redirected |
| `EVETEST_COLLECT_ARTIFACTS` | Host path for artifacts (logs, collect-info) | -- |
| `EVETEST_COLLECT_COVERAGE` | Collect Go coverage profiles (requires `EVETEST_COLLECT_ARTIFACTS` and EVE built with `COVER=y`) | `false` |
| `EVETEST_REGISTRY_MIRROR_DOCKER` | Pull-through cache URL(s) for docker.io — one or more comma-separated `[scheme://]host:port[/path]` (IPv6 hosts bracketed, e.g. `http://[fd11::5]:5000`); see `RequireIPv6OnlyRegistryMirrors` | -- |
Expand Down
11 changes: 11 additions & 0 deletions evetest/constants/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ const (
// This is read by both the evetest container and the broker.
LogLevelEnv = "LOG_LEVEL"

// ColorOutputEnv determines whether framework log output is colorized
// with ANSI escape codes. When unset, the Makefile (or, as a fallback,
// the container entrypoint) enables colors only if stdout is attached
// to a terminal, so that piped or redirected output stays free of
// escape codes.
ColorOutputEnv = "COLOR_OUTPUT"

// APIAddressEnv specifies the IP address on which the evetest container exposes
// its gRPC API.
// This is used by the evetest CLI to connect to a running evetest instance.
Expand Down Expand Up @@ -382,6 +389,10 @@ func InitViperConfig() {

// Logging
viper.SetDefault(LogLevelEnv, DefaultLogLevel)
// The Makefile and the entrypoint script override this based on TTY
// presence; the default only applies when the framework is run outside
// the evetest container.
viper.SetDefault(ColorOutputEnv, true)

// gRPC API ports and addresses
viper.SetDefault(APIAddressEnv, "")
Expand Down
21 changes: 18 additions & 3 deletions evetest/devconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -1047,6 +1047,10 @@ func (config ApplicationInstanceConfig) toProto(th *TestHarness, devName string,
Id: aclID,
})
}
interfaceOrder := uint32(i)
if adapter.InterfaceOrder != nil {
interfaceOrder = *adapter.InterfaceOrder
}
appInstConfig.Interfaces = append(appInstConfig.Interfaces,
&eveconfig.NetworkAdapter{
Name: adapter.LogicalLabel,
Expand All @@ -1055,7 +1059,7 @@ func (config ApplicationInstanceConfig) toProto(th *TestHarness, devName string,
MacAddress: adapter.MAC.String(),
Acls: acls,
AccessVlanId: uint32(adapter.AccessVLAN),
InterfaceOrder: uint32(i),
InterfaceOrder: interfaceOrder,
})
}
}
Expand Down Expand Up @@ -1397,6 +1401,7 @@ type VirtualNetworkAdapter struct {
AccessVLAN uint16
PortFwdRules []PortFwdRule
ACLAllowRules []ACLAllowRule
InterfaceOrder *uint32
}

func (VirtualNetworkAdapter) isAppNetworkAdapter() {}
Expand Down Expand Up @@ -2396,18 +2401,19 @@ func (dc *EdgeDeviceConfig) UpdateApplication(
if !proto.Equal(app.Fixedresources, newProtoConfig.Fixedresources) {
dc.th.t.Fatalf("It is not allowed to change application Fixedresources")
}
var needRestart bool
var needPurge bool
equalAdapter := func(a1, a2 *eveconfig.Adapter) bool {
return proto.Equal(a1, a2)
}
if !generics.EqualSetsFn(app.Adapters, newProtoConfig.Adapters, equalAdapter) {
needPurge = true
needRestart = true
}
equalNetAdapter := func(a1, a2 *eveconfig.NetworkAdapter) bool {
return proto.Equal(a1, a2)
}
if !generics.EqualSetsFn(app.Interfaces, newProtoConfig.Interfaces, equalNetAdapter) {
needPurge = true
needRestart = true
}
// The root ref (VolumeRefList[0]) is always left untouched;
// buildMountRefs only ever references existing volumes, it does
Expand All @@ -2420,11 +2426,20 @@ func (dc *EdgeDeviceConfig) UpdateApplication(
if !generics.EqualSetsFn(app.VolumeRefList[1:], newMountRefs, equalVolumeRef) {
needPurge = true
}
// A purge subsumes a restart -- it stops the app, recreates its
// volumes and starts it again, which also applies any adapter
// change that is otherwise staged until the next restart. So when
// both are needed, bumping the purge counter alone is enough.
if needPurge {
if app.Purge == nil {
app.Purge = &eveconfig.InstanceOpsCmd{Counter: 0}
}
app.Purge.Counter++
} else if needRestart {
if app.Restart == nil {
app.Restart = &eveconfig.InstanceOpsCmd{Counter: 0}
}
app.Restart.Counter++
}
dc.Apps[i].Activate = newProtoConfig.Activate
dc.Apps[i].ProfileList = newProtoConfig.ProfileList
Expand Down
14 changes: 14 additions & 0 deletions evetest/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ EOF

chmod +x /usr/local/bin/adam-cli

# Fallback when EVETEST_COLOR_OUTPUT was not passed in (the Makefile sets it
# based on whether the host stdout is a terminal): colorize framework log
# output only when stdout is attached to a terminal, i.e. when 'docker run'
# was invoked manually with a pseudo-TTY. This keeps piped or redirected
# output free of ANSI escape codes.
if [ -z "$EVETEST_COLOR_OUTPUT" ]; then
if [ -t 1 ]; then
EVETEST_COLOR_OUTPUT=true
else
EVETEST_COLOR_OUTPUT=false
fi
fi
export EVETEST_COLOR_OUTPUT

# Run go test in background
GO_TEST_FLAGS="-v"
GO_TEST_OUTPUT_FILE="${EVETEST_ARTIFACT_DIR}/gotest.txt"
Expand Down
25 changes: 18 additions & 7 deletions evetest/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,10 +214,11 @@ const (
type TestHarness struct {
api.UnimplementedEvetestServer

t *T
log *logrus.Logger
userLog *logrus.Logger
brokerLog *logrus.Logger
t *T
log *logrus.Logger
userLog *logrus.Logger
brokerLog *logrus.Logger
colorOutput bool

artifactDir string

Expand Down Expand Up @@ -446,6 +447,15 @@ func removeStaleImageCacheDirs(log *logrus.Logger, imgCacheParent string) {
}
}

// prefixColor returns the given log-prefix color when colorized output is
// enabled, PrefixColorNone otherwise.
func (th *TestHarness) prefixColor(c logger.PrefixColor) logger.PrefixColor {
if th.colorOutput {
return c
}
return logger.PrefixColorNone
}

// Init initializes the test harness and must be called exactly once per test.
// When used inside a test suite, Init may be called multiple times, once per
// test case, but only a single harness instance will be created.
Expand Down Expand Up @@ -508,24 +518,25 @@ func Init(t *testing.T) *T {
if err != nil {
th.t.Fatalf("Failed to parse log level %q: %v", logLevelStr, err)
}
th.colorOutput = viper.GetBool(constants.ColorOutputEnv)
th.log = logrus.New()
th.log.SetFormatter(&logger.PrefixedFormatter{
Prefix: "HARNESS ",
Color: logger.PrefixColorBlue,
Color: th.prefixColor(logger.PrefixColorBlue),
})
th.log.SetLevel(logLevel)
th.userLog = logrus.New()
th.userLog.SetFormatter(&logger.PrefixedFormatter{
Prefix: "TEST ",
Color: logger.PrefixColorCyan,
Color: th.prefixColor(logger.PrefixColorCyan),
})
th.userLog.SetLevel(logLevel)

// Setup logging for logs coming from the broker.
th.brokerLog = logrus.New()
th.brokerLog.SetFormatter(&logger.PrefixedFormatter{
Prefix: "BROKER ",
Color: logger.PrefixColorPurple,
Color: th.prefixColor(logger.PrefixColorPurple),
})
th.brokerLog.SetLevel(logLevel)

Expand Down
8 changes: 6 additions & 2 deletions evetest/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,12 @@ const (
func (t *T) fail(msg string, now bool) {
t.Helper()

// Log the error message with the red color.
t.Log(redColor + "TEST FAILURE: " + msg + resetColor)
// Log the error message, highlighted in red when colors are enabled.
failureMsg := "TEST FAILURE: " + msg
if t.th.colorOutput {
failureMsg = redColor + failureMsg + resetColor
}
t.Log(failureMsg)

// Log stacktrace at the point of failure for easier debugging.
t.Logf("STACKTRACE:\n%s", debug.Stack())
Expand Down
Loading
Loading