Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68f7431d98 | ||
|
|
53749833bf |
-303
@@ -34,306 +34,3 @@ modules/mesh-build-image/cmd/mesh-build-image/mesh-build-image
|
||||
/checks/paths/paths
|
||||
/modules/mesh-build-go/mesh-build-go
|
||||
/modules/mesh-delivery-gate/mesh-delivery-gate
|
||||
|
||||
# A binary built in place, named like its module directory or a cmd directory, in the module root or in that cmd directory
|
||||
# (issue 1921, try speed S13), for every module; the lint step of every check refuses one a change adds, a module added
|
||||
# later included.
|
||||
/modules/adwaita/adwaita
|
||||
/modules/adwaita/adwaita-tools
|
||||
/modules/adwaita/cmd/adwaita-tools/adwaita-tools
|
||||
/modules/artifact-store-tools/artifact-store-tools
|
||||
/modules/artifact-store-tools/cmd/artifact-store-tools/artifact-store-tools
|
||||
/modules/asus-zephyrus-g14/asus-zephyrus-g14
|
||||
/modules/asus-zephyrus-g14/zephyrus
|
||||
/modules/asus-zephyrus-g14/cmd/zephyrus/zephyrus
|
||||
/modules/audit-logger/audit-logger
|
||||
/modules/avahi/avahi
|
||||
/modules/avahi/avahi-tools
|
||||
/modules/avahi/cmd/avahi-tools/avahi-tools
|
||||
/modules/baserow/baserow
|
||||
/modules/blueman/blueman
|
||||
/modules/blueman/blueman-tools
|
||||
/modules/blueman/cmd/blueman-tools/blueman-tools
|
||||
/modules/bluetooth/bluetooth
|
||||
/modules/bluetooth/bluetooth-tools
|
||||
/modules/bluetooth/cmd/bluetooth-tools/bluetooth-tools
|
||||
/modules/brother/brother
|
||||
/modules/brother/cmd/brother/brother
|
||||
/modules/buildah/buildah
|
||||
/modules/ca-trust/ca-trust
|
||||
/modules/claude-code/claude-code
|
||||
/modules/claude-code/cmd/claude-code/claude-code
|
||||
/modules/claude-licence-manager/claude-licence-manager
|
||||
/modules/claude-licence-manager/cmd/claude-licence-manager/claude-licence-manager
|
||||
/modules/clipmenu/clipmenu
|
||||
/modules/clipmenu/clipmenu-tools
|
||||
/modules/clipmenu/cmd/clipmenu-tools/clipmenu-tools
|
||||
/modules/confluence/confluence
|
||||
/modules/cups/cups
|
||||
/modules/cups/cups-tools
|
||||
/modules/cups/cmd/cups-tools/cups-tools
|
||||
/modules/dbus/dbus
|
||||
/modules/dbus/dbus-tools
|
||||
/modules/dbus/cmd/dbus-tools/dbus-tools
|
||||
/modules/desk-channel/desk-channel
|
||||
/modules/desk-channel/cmd/desk-channel/desk-channel
|
||||
/modules/dev-tools/dev-tools
|
||||
/modules/disk-load/disk-load
|
||||
/modules/disk-load/cmd/disk-load/disk-load
|
||||
/modules/distribution/distribution
|
||||
/modules/dmenu/dmenu
|
||||
/modules/dmenu/dmenu-tools
|
||||
/modules/dmenu/cmd/dmenu-tools/dmenu-tools
|
||||
/modules/dnsmasq/dnsmasq
|
||||
/modules/dnsmasq/dnsmasq-tools
|
||||
/modules/dnsmasq/cmd/dnsmasq-tools/dnsmasq-tools
|
||||
/modules/docker/docker
|
||||
/modules/docker/docker-tools
|
||||
/modules/docker/cmd/docker-tools/docker-tools
|
||||
/modules/docker-compose/docker-compose
|
||||
/modules/docker-compose/docker-compose-tools
|
||||
/modules/docker-compose/cmd/docker-compose-tools/docker-compose-tools
|
||||
/modules/dunst/dunst
|
||||
/modules/dunst/dunst-tools
|
||||
/modules/dunst/cmd/dunst-tools/dunst-tools
|
||||
/modules/fail2ban/fail2ban
|
||||
/modules/fail2ban/fail2ban-tools
|
||||
/modules/fail2ban/cmd/fail2ban-tools/fail2ban-tools
|
||||
/modules/feh/feh
|
||||
/modules/feh/feh-tools
|
||||
/modules/feh/cmd/feh-tools/feh-tools
|
||||
/modules/flatpak/flatpak
|
||||
/modules/flatpak/flatpak-tools
|
||||
/modules/flatpak/cmd/flatpak-tools/flatpak-tools
|
||||
/modules/fonts/fonts
|
||||
/modules/fonts/fonts-tools
|
||||
/modules/fonts/cmd/fonts-tools/fonts-tools
|
||||
/modules/forticlient/forticlient
|
||||
/modules/forticlient/forticlient-tools
|
||||
/modules/forticlient/cmd/forticlient-tools/forticlient-tools
|
||||
/modules/gitea/gitea
|
||||
/modules/gitea/agent-teams
|
||||
/modules/gitea/cmd/agent-teams/agent-teams
|
||||
/modules/gitea/agent-token
|
||||
/modules/gitea/cmd/agent-token/agent-token
|
||||
/modules/gitea/forge
|
||||
/modules/gitea/cmd/forge/forge
|
||||
/modules/gitea/forge-account-provider
|
||||
/modules/gitea/cmd/forge-account-provider/forge-account-provider
|
||||
/modules/gitea/npm-registry
|
||||
/modules/gitea/cmd/npm-registry/npm-registry
|
||||
/modules/gitea/repositories
|
||||
/modules/gitea/cmd/repositories/repositories
|
||||
/modules/gitlab/gitlab
|
||||
/modules/gnome-keyring/gnome-keyring
|
||||
/modules/gnome-keyring/gnome-keyring-tools
|
||||
/modules/gnome-keyring/cmd/gnome-keyring-tools/gnome-keyring-tools
|
||||
/modules/go/go
|
||||
/modules/grafana/grafana
|
||||
/modules/hello-web/hello-web
|
||||
/modules/home-assistant/home-assistant
|
||||
/modules/hostname/hostname
|
||||
/modules/hostname/hostname-tools
|
||||
/modules/hostname/cmd/hostname-tools/hostname-tools
|
||||
/modules/i3/i3
|
||||
/modules/i3/i3-tools
|
||||
/modules/i3/cmd/i3-tools/i3-tools
|
||||
/modules/i3status-rust/i3status-rust
|
||||
/modules/i3status-rust/i3status-rust-tools
|
||||
/modules/i3status-rust/cmd/i3status-rust-tools/i3status-rust-tools
|
||||
/modules/icecast/icecast
|
||||
/modules/influxdb/influxdb
|
||||
/modules/invoicing/invoicing
|
||||
/modules/jira/jira
|
||||
/modules/kernel/kernel
|
||||
/modules/kernel/kernel-tools
|
||||
/modules/kernel/cmd/kernel-tools/kernel-tools
|
||||
/modules/keycloak/keycloak
|
||||
/modules/keycloak/keycloak-provider
|
||||
/modules/keycloak/cmd/keycloak-provider/keycloak-provider
|
||||
/modules/lab/lab
|
||||
/modules/lemurs/lemurs
|
||||
/modules/lemurs/lemurs-tools
|
||||
/modules/lemurs/cmd/lemurs-tools/lemurs-tools
|
||||
/modules/letta/letta
|
||||
/modules/localization/localization
|
||||
/modules/localization/localization-tools
|
||||
/modules/localization/cmd/localization-tools/localization-tools
|
||||
/modules/local-model-consumer/local-model-consumer
|
||||
/modules/logrotate/logrotate
|
||||
/modules/logrotate/logrotate-tools
|
||||
/modules/logrotate/cmd/logrotate-tools/logrotate-tools
|
||||
/modules/mailu/mailu
|
||||
/modules/mailu/automx
|
||||
/modules/mailu/cmd/automx/automx
|
||||
/modules/marrytts/marrytts
|
||||
/modules/matrix/matrix
|
||||
/modules/memory-pressure/memory-pressure
|
||||
/modules/memory-pressure/cmd/memory-pressure/memory-pressure
|
||||
/modules/mesh-build-go/mesh-build-go
|
||||
/modules/mesh-build-go/cmd/mesh-build-go/mesh-build-go
|
||||
/modules/mesh-build-image/mesh-build-image
|
||||
/modules/mesh-build-image/cmd/mesh-build-image/mesh-build-image
|
||||
/modules/mesh-build-node/mesh-build-node
|
||||
/modules/mesh-build-node/cmd/mesh-build-node/mesh-build-node
|
||||
/modules/mesh-catalog/mesh-catalog
|
||||
/modules/mesh-cli/mesh-cli
|
||||
/modules/mesh-cli/cmd/mesh-cli/mesh-cli
|
||||
/modules/mesh-config/mesh-config
|
||||
/modules/mesh-config/cmd/mesh-config/mesh-config
|
||||
/modules/mesh-delivery-gate/mesh-delivery-gate
|
||||
/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/mesh-delivery-gate
|
||||
/modules/mesh-files/mesh-files
|
||||
/modules/mesh-files/cmd/mesh-files/mesh-files
|
||||
/modules/mesh-issues/mesh-issues
|
||||
/modules/mesh-issues/cmd/mesh-issues/mesh-issues
|
||||
/modules/mesh-knowledge/mesh-knowledge
|
||||
/modules/mesh-knowledge/cmd/mesh-knowledge/mesh-knowledge
|
||||
/modules/mesh-mail/mesh-mail
|
||||
/modules/mesh-mail/cmd/mesh-mail/mesh-mail
|
||||
/modules/mesh-mail-notifier/mesh-mail-notifier
|
||||
/modules/mesh-mail-notifier/cmd/mesh-mail-notifier/mesh-mail-notifier
|
||||
/modules/mesh-mail-reader/mesh-mail-reader
|
||||
/modules/mesh-mail-reader/cmd/mesh-mail-reader/mesh-mail-reader
|
||||
/modules/mesh-mail-web/mesh-mail-web
|
||||
/modules/mesh-mail-web/cmd/mesh-mail-web/mesh-mail-web
|
||||
/modules/mesh-provisioner/mesh-provisioner
|
||||
/modules/mesh-provisioner/cmd/mesh-provisioner/mesh-provisioner
|
||||
/modules/mesh-provisioner-client/mesh-provisioner-client
|
||||
/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/mesh-provisioner-client
|
||||
/modules/mesh-reads/mesh-reads
|
||||
/modules/mesh-reads/cmd/mesh-reads/mesh-reads
|
||||
/modules/mesh-vault/mesh-vault
|
||||
/modules/mesh-vault/cmd/mesh-vault/mesh-vault
|
||||
/modules/mesh-watcher/mesh-watcher
|
||||
/modules/mesh-watcher/cmd/mesh-watcher/mesh-watcher
|
||||
/modules/messenger/messenger
|
||||
/modules/messenger/cmd/messenger/messenger
|
||||
/modules/minio/minio
|
||||
/modules/minio/object-store
|
||||
/modules/minio/cmd/object-store/object-store
|
||||
/modules/model-usage/model-usage
|
||||
/modules/mongodb/mongodb
|
||||
/modules/mosquitto/mosquitto
|
||||
/modules/mounts/mounts
|
||||
/modules/mounts/cmd/mounts/mounts
|
||||
/modules/mssql/mssql
|
||||
/modules/n8n/n8n
|
||||
/modules/nats/nats
|
||||
/modules/nats/mesh-nats-snapshot
|
||||
/modules/nats/cmd/mesh-nats-snapshot/mesh-nats-snapshot
|
||||
/modules/nats/nats-tools
|
||||
/modules/nats/cmd/nats-tools/nats-tools
|
||||
/modules/netcheck/netcheck
|
||||
/modules/netcheck/cmd/netcheck/netcheck
|
||||
/modules/networkmanager/networkmanager
|
||||
/modules/networkmanager/uplink-tools
|
||||
/modules/networkmanager/cmd/uplink-tools/uplink-tools
|
||||
/modules/nextcloud/nextcloud
|
||||
/modules/nextcloud-client/nextcloud-client
|
||||
/modules/nextcloud-client/nextcloud-client-tools
|
||||
/modules/nextcloud-client/cmd/nextcloud-client-tools/nextcloud-client-tools
|
||||
/modules/nfs-server/nfs-server
|
||||
/modules/nfs-server/cmd/nfs-server/nfs-server
|
||||
/modules/nftables/nftables
|
||||
/modules/nm-applet/nm-applet
|
||||
/modules/nm-applet/nm-applet-tools
|
||||
/modules/nm-applet/cmd/nm-applet-tools/nm-applet-tools
|
||||
/modules/node-env/node-env
|
||||
/modules/nodejs/nodejs
|
||||
/modules/nodered/nodered
|
||||
/modules/ollama/ollama
|
||||
/modules/only-office/only-office
|
||||
/modules/only-office/cmd/only-office/only-office
|
||||
/modules/openai-consumer/openai-consumer
|
||||
/modules/openrazer/openrazer
|
||||
/modules/openrazer/openrazer-tools
|
||||
/modules/openrazer/cmd/openrazer-tools/openrazer-tools
|
||||
/modules/pacman/pacman
|
||||
/modules/pacman/pacman-tools
|
||||
/modules/pacman/cmd/pacman-tools/pacman-tools
|
||||
/modules/photos-eef/photos-eef
|
||||
/modules/photos-filip/photos-filip
|
||||
/modules/picom/picom
|
||||
/modules/picom/picom-tools
|
||||
/modules/picom/cmd/picom-tools/picom-tools
|
||||
/modules/polychromatic/polychromatic
|
||||
/modules/polychromatic/polychromatic-tools
|
||||
/modules/polychromatic/cmd/polychromatic-tools/polychromatic-tools
|
||||
/modules/postgres/postgres
|
||||
/modules/postgres/postgres-provider
|
||||
/modules/postgres/cmd/postgres-provider/postgres-provider
|
||||
/modules/power/power
|
||||
/modules/power/cmd/power/power
|
||||
/modules/powerlevel10k/powerlevel10k
|
||||
/modules/records/records
|
||||
/modules/records/cmd/records/records
|
||||
/modules/redis/redis
|
||||
/modules/restic/restic
|
||||
/modules/restic/restic-backups
|
||||
/modules/restic/cmd/restic-backups/restic-backups
|
||||
/modules/rofi/rofi
|
||||
/modules/rofi/rofi-tools
|
||||
/modules/rofi/cmd/rofi-tools/rofi-tools
|
||||
/modules/route-adapter/route-adapter
|
||||
/modules/route-adapter/route-provider
|
||||
/modules/route-adapter/cmd/route-provider/route-provider
|
||||
/modules/screen-lock/screen-lock
|
||||
/modules/screen-lock/screen-lock-tools
|
||||
/modules/screen-lock/cmd/screen-lock-tools/screen-lock-tools
|
||||
/modules/searxng/searxng
|
||||
/modules/sensors/sensors
|
||||
/modules/sensors/cmd/sensors/sensors
|
||||
/modules/showcase/showcase
|
||||
/modules/slack/slack
|
||||
/modules/slack/slack-tools
|
||||
/modules/slack/cmd/slack-tools/slack-tools
|
||||
/modules/snapd/snapd
|
||||
/modules/snapd/snapd-tools
|
||||
/modules/snapd/cmd/snapd-tools/snapd-tools
|
||||
/modules/ssh-client/ssh-client
|
||||
/modules/ssh-client/ssh-client-tools
|
||||
/modules/ssh-client/cmd/ssh-client-tools/ssh-client-tools
|
||||
/modules/sshd/sshd
|
||||
/modules/step-ca/step-ca
|
||||
/modules/sudo/sudo
|
||||
/modules/sudo/sudo-tools
|
||||
/modules/sudo/cmd/sudo-tools/sudo-tools
|
||||
/modules/supabase/supabase
|
||||
/modules/systemd/systemd
|
||||
/modules/systemd/systemd-tools
|
||||
/modules/systemd/cmd/systemd-tools/systemd-tools
|
||||
/modules/systemd-networkd/systemd-networkd
|
||||
/modules/systemd-networkd/uplink-tools
|
||||
/modules/systemd-networkd/cmd/uplink-tools/uplink-tools
|
||||
/modules/systemd-resolved/systemd-resolved
|
||||
/modules/systemd-resolved/resolver-tools
|
||||
/modules/systemd-resolved/cmd/resolver-tools/resolver-tools
|
||||
/modules/telegram/telegram
|
||||
/modules/telegram/cmd/telegram/telegram
|
||||
/modules/time-sync/time-sync
|
||||
/modules/time-sync/time-sync-tools
|
||||
/modules/time-sync/cmd/time-sync-tools/time-sync-tools
|
||||
/modules/triggerhappy/triggerhappy
|
||||
/modules/triggerhappy/triggerhappy-tools
|
||||
/modules/triggerhappy/cmd/triggerhappy-tools/triggerhappy-tools
|
||||
/modules/umami/umami
|
||||
/modules/unifi/unifi
|
||||
/modules/website/website
|
||||
/modules/xclip/xclip
|
||||
/modules/xclip/xclip-tools
|
||||
/modules/xclip/cmd/xclip-tools/xclip-tools
|
||||
/modules/xdg/xdg
|
||||
/modules/xdg/xdg-tools
|
||||
/modules/xdg/cmd/xdg-tools/xdg-tools
|
||||
/modules/xorg/xorg
|
||||
/modules/xorg/cmd/xorg/xorg
|
||||
/modules/xterm/xterm
|
||||
/modules/xterm/xterm-tools
|
||||
/modules/xterm/cmd/xterm-tools/xterm-tools
|
||||
/modules/zfs/zfs
|
||||
/modules/zfs/zfs-tools
|
||||
/modules/zfs/cmd/zfs-tools/zfs-tools
|
||||
/modules/zsh/zsh
|
||||
/modules/zsh-autosuggestions/zsh-autosuggestions
|
||||
/modules/zsh-syntax-highlighting/zsh-syntax-highlighting
|
||||
|
||||
Binary file not shown.
@@ -1,90 +0,0 @@
|
||||
package main
|
||||
|
||||
// Stray build output (issue 1921, try speed S13): the lint step fails a change that adds an executable (ELF or Mach-O) or
|
||||
// a file over MaxAddedFile, naming each, unless the repository's .binaries-allowed lists its path. Build binaries were
|
||||
// committed three times in one night; the forge keeps them for ever.
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
)
|
||||
|
||||
// MaxAddedFile is the largest file a change may add without .binaries-allowed naming it.
|
||||
const MaxAddedFile = 5 << 20
|
||||
|
||||
// BinariesAllowed is the file in a repository's root listing, one path per line ("#" starts a comment), the executables
|
||||
// and large files it keeps on purpose.
|
||||
const BinariesAllowed = ".binaries-allowed"
|
||||
|
||||
// executableMagic are the first bytes of an ELF and of each Mach-O form (32 and 64 bits, both byte orders, universal).
|
||||
var executableMagic = [][]byte{[]byte("\x7fELF"), {0xfe, 0xed, 0xfa, 0xce}, {0xfe, 0xed, 0xfa, 0xcf}, {0xce, 0xfa, 0xed, 0xfe},
|
||||
{0xcf, 0xfa, 0xed, 0xfe}, {0xca, 0xfe, 0xba, 0xbe}}
|
||||
|
||||
// addedBinaries is the lint finding of the files the merge adds to the trunk that are executables or over MaxAddedFile,
|
||||
// nil when there is none. The paths come from git (the merge commit against its first parent, the trunk); a tree with no
|
||||
// merge (no first parent) adds nothing to judge.
|
||||
func (m *Module) addedBinaries(ctx context.Context, run forgekey.Runner, tree string) []Failure {
|
||||
r := run(ctx, []string{"GIT_TERMINAL_PROMPT=0"}, "git", "-C", tree, "diff", "--name-only", "--no-renames", "--diff-filter=A", "HEAD^1", "HEAD")
|
||||
if r.Status != 0 || r.Err != "" {
|
||||
return nil
|
||||
}
|
||||
allowed := map[string]bool{}
|
||||
if raw, err := os.ReadFile(filepath.Join(tree, BinariesAllowed)); err == nil {
|
||||
scan := bufio.NewScanner(bytes.NewReader(raw))
|
||||
for scan.Scan() {
|
||||
if l := strings.TrimSpace(scan.Text()); l != "" && !strings.HasPrefix(l, "#") {
|
||||
allowed[filepath.ToSlash(l)] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
var found []string
|
||||
for _, p := range strings.Split(r.Stdout, "\n") {
|
||||
if p = strings.TrimSpace(p); p == "" || allowed[p] {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(tree, filepath.FromSlash(p))
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil || !info.Mode().IsRegular() {
|
||||
continue // narrowed away, or not a file
|
||||
}
|
||||
if info.Size() > MaxAddedFile {
|
||||
found = append(found, fmt.Sprintf("%s (%d bytes, over %d)", p, info.Size(), MaxAddedFile))
|
||||
continue
|
||||
}
|
||||
if isExecutable(path) {
|
||||
found = append(found, p+" (an executable)")
|
||||
}
|
||||
}
|
||||
if len(found) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.Strings(found)
|
||||
return []Failure{{Package: ".", Test: "binaries", Step: StepLint,
|
||||
Output: "the change adds build output or a large file (remove it, or list its path in " + BinariesAllowed + "): " + strings.Join(found, ", ")}}
|
||||
}
|
||||
|
||||
func isExecutable(path string) bool {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
head := make([]byte, 4)
|
||||
if n, _ := f.Read(head); n < 4 {
|
||||
return false
|
||||
}
|
||||
for _, magic := range executableMagic {
|
||||
if bytes.Equal(head, magic) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
)
|
||||
|
||||
// Issue 1921, try speed S13: the lint step fails a change that adds an executable (ELF or Mach-O) or a file over 5 MB,
|
||||
// naming it, unless .binaries-allowed in the repository's root lists its path; a file the change does not add is not
|
||||
// judged, and neither is an allowed one.
|
||||
func TestTheLintStepFailsAChangeThatAddsABinaryOrALargeFile(t *testing.T) {
|
||||
m, states, ws, _ := workspaceFor(t, "slow")
|
||||
tree := filepath.Join(m.Work, ws, "tree")
|
||||
write := func(name string, body []byte) {
|
||||
os.MkdirAll(filepath.Dir(filepath.Join(tree, name)), 0o755)
|
||||
if err := os.WriteFile(filepath.Join(tree, name), body, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
write("go.mod", []byte("module fixture/slow\n"))
|
||||
elf := append([]byte("\x7fELF"), make([]byte, 64)...)
|
||||
macho := append([]byte{0xcf, 0xfa, 0xed, 0xfe}, make([]byte, 64)...)
|
||||
write("modules/x/cmd/x/x", elf)
|
||||
write("modules/y/y", macho)
|
||||
write("big.tar", make([]byte, 5<<20+1))
|
||||
write("tools/allowed-tool", elf)
|
||||
write("old/binary", elf) // there before the change
|
||||
write("modules/x/doc.md", []byte("text\n"))
|
||||
write(".binaries-allowed", []byte("# kept on purpose\ntools/allowed-tool\n"))
|
||||
inner := m.Run
|
||||
m.Run = func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
|
||||
if name == "git" && strings.Contains(strings.Join(args, " "), "--diff-filter=A") {
|
||||
return forgekey.Ran{Stdout: "modules/x/cmd/x/x\nmodules/y/y\nbig.tar\ntools/allowed-tool\nmodules/x/doc.md\n.binaries-allowed\n"}
|
||||
}
|
||||
return inner(ctx, env, name, args...)
|
||||
}
|
||||
res, st := runUnit(t, m, states, ws)
|
||||
if st.State != delivery.StepPassed {
|
||||
t.Fatalf("the step ended %s: %s", st.State, st.Reason)
|
||||
}
|
||||
var found []Failure
|
||||
for _, f := range res.Failed {
|
||||
if f.Test == "binaries" {
|
||||
found = append(found, f)
|
||||
}
|
||||
}
|
||||
if res.Passed || len(found) != 1 || found[0].Step != StepLint {
|
||||
t.Fatalf("failed = %+v, want one lint finding of the binaries", res.Failed)
|
||||
}
|
||||
out := found[0].Output
|
||||
for _, want := range []string{"modules/x/cmd/x/x", "modules/y/y", "big.tar"} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("the finding does not name %s: %s", want, out)
|
||||
}
|
||||
}
|
||||
for _, not := range []string{"allowed-tool", "old/binary", "doc.md"} {
|
||||
if strings.Contains(out, not) {
|
||||
t.Errorf("the finding names %s: %s", not, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// 1921/S11 (the lead's decision): a check compiles and runs with CGO_ENABLED=0, as a production build does, so it tests
|
||||
// what ships and the warm cache matches; the lint's go vet, a compile alone, writes the shared cache, while a go test that
|
||||
// runs a test binary reads it beneath the ask's own layer.
|
||||
func TestACheckRunsWithoutCgoAndOnlyItsCompileWritesTheSharedCache(t *testing.T) {
|
||||
m, states, ws, calls := workspaceFor(t, "failing")
|
||||
var seen []shutOff
|
||||
m.Run = throughBubblewrap(t, m.Run, &seen)
|
||||
if _, st := runUnit(t, m, states, ws); st.State != delivery.StepPassed {
|
||||
t.Fatalf("the step ended %s: %s", st.State, st.Reason)
|
||||
}
|
||||
for _, c := range *calls {
|
||||
if strings.HasPrefix(c.line, "go ") && !slices.Contains(c.env, "CGO_ENABLED=0") {
|
||||
t.Errorf("%s ran without CGO_ENABLED=0: %v", c.line, c.env)
|
||||
}
|
||||
}
|
||||
vets, tests := 0, 0
|
||||
for _, s := range seen {
|
||||
switch {
|
||||
case strings.Contains(s.command, " vet "):
|
||||
vets++
|
||||
checkShutOff(t, m, s, true) // the shared caches bound read-write
|
||||
case strings.Contains(s.command, " test "):
|
||||
tests++
|
||||
checkShutOff(t, m, s, false)
|
||||
}
|
||||
}
|
||||
if vets == 0 || tests == 0 {
|
||||
t.Errorf("%d vets and %d tests ran shut off, want both", vets, tests)
|
||||
}
|
||||
}
|
||||
@@ -49,51 +49,6 @@ type FetchResult struct {
|
||||
// Beside are the sibling repositories placed as plain directories under the workspace's beside directory, which
|
||||
// build_go_test hands to the tests as MESH_CHECK_BESIDE (ADR 0484 clause 4).
|
||||
Beside []Sibling `json:"beside,omitempty"`
|
||||
// Took is how long each phase of the fetch ran, in seconds (issue 1921, the try speed-up's first measurement): what
|
||||
// the checker reports as one "clone" time, split.
|
||||
Took *FetchTook `json:"took,omitempty"`
|
||||
}
|
||||
|
||||
// FetchTook is the time of each phase of a fetch, in seconds; its phases add up to Total. Mirror is bringing the
|
||||
// repository's mirror to the commit, Fetch the fetch and checkout into the tree, Merge the merge preview, Dissociate
|
||||
// the repack that makes the tree independent of the mirror, Beside the placing of the siblings (their own mirror,
|
||||
// fetch and repack), Other the narrowing and the listing of the go.mod directories, and Modules the download and
|
||||
// verify of the Go modules.
|
||||
type FetchTook struct {
|
||||
Mirror float64 `json:"mirror"`
|
||||
Fetch float64 `json:"fetch"`
|
||||
Merge float64 `json:"preview"` // named for the merge preview, apart from the result's merge commit
|
||||
Dissociate float64 `json:"dissociate"`
|
||||
Beside float64 `json:"siblings"`
|
||||
Other float64 `json:"other"`
|
||||
Modules float64 `json:"modules"`
|
||||
Total float64 `json:"total"`
|
||||
}
|
||||
|
||||
// phaseClock stamps the phases of a fetch: each lap is the time since the one before, so the laps add up to the total.
|
||||
type phaseClock struct {
|
||||
now func() time.Time
|
||||
start, since time.Time
|
||||
}
|
||||
|
||||
func newPhaseClock(now func() time.Time) *phaseClock {
|
||||
t := now()
|
||||
return &phaseClock{now: now, start: t, since: t}
|
||||
}
|
||||
|
||||
// lap adds the time since the last lap to *into.
|
||||
func (c *phaseClock) lap(into *float64) {
|
||||
t := c.now()
|
||||
*into += t.Sub(c.since).Seconds()
|
||||
c.since = t
|
||||
}
|
||||
|
||||
func (c *phaseClock) total() float64 { return c.since.Sub(c.start).Seconds() }
|
||||
|
||||
// String is the one log line a fetch says of its phases.
|
||||
func (t FetchTook) String() string {
|
||||
return fmt.Sprintf("mirror %.1fs, fetch %.1fs, merge %.1fs, dissociate %.1fs, beside %.1fs, other %.1fs, modules %.1fs, total %.1fs",
|
||||
t.Mirror, t.Fetch, t.Merge, t.Dissociate, t.Beside, t.Other, t.Modules, t.Total)
|
||||
}
|
||||
|
||||
// fetchTool is the fetch step tool.
|
||||
@@ -154,77 +109,43 @@ func (m *Module) fetch(ctx context.Context, args map[string]any) (res any, err e
|
||||
}
|
||||
|
||||
func (m *Module) fetchInto(ctx context.Context, req buildstep.FetchRequest, id string, siblings []siblingArg) (any, error) {
|
||||
res, err := m.fetchPhases(ctx, req, id, siblings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if m.Say != nil && res.Took != nil {
|
||||
m.Say("the fetch of %s at %s took %s", req.Repository, short(req.Commit), res.Took)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func short(commit string) string {
|
||||
if len(commit) > 12 {
|
||||
return commit[:12]
|
||||
}
|
||||
return commit
|
||||
}
|
||||
|
||||
func (m *Module) fetchPhases(ctx context.Context, req buildstep.FetchRequest, id string, siblings []siblingArg) (FetchResult, error) {
|
||||
repository, commit, ref, base := req.Repository, req.Commit, req.Ref, req.Base
|
||||
var err error
|
||||
took := &FetchTook{}
|
||||
clock := newPhaseClock(m.now)
|
||||
defer func() { took.Total = clock.total() }()
|
||||
root := filepath.Join(m.Work, id)
|
||||
tree := filepath.Join(root, "tree")
|
||||
if err := os.MkdirAll(tree, 0o700); err != nil {
|
||||
return FetchResult{}, err
|
||||
return nil, err
|
||||
}
|
||||
remote := m.remote(repository)
|
||||
opts := m.fetchOptions(ctx, repository, commit, ref)
|
||||
mergeOpts := m.FetchOptions
|
||||
if base != "" {
|
||||
// The mirror is brought to the trunk commit too, so the merge preview deepens the tree from it and asks the forge
|
||||
// for no history the mirror holds (issue 1921, try speed S6).
|
||||
mergeOpts = m.fetchOptions(ctx, repository, base, "")
|
||||
if err := buildstep.FetchCommit(ctx, m.Run, tree, remote, commit, ref, m.fetchOptions(ctx, repository, commit, ref)...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clock.lap(&took.Mirror)
|
||||
if err := buildstep.FetchCommit(ctx, m.Run, tree, remote, commit, ref, opts...); err != nil {
|
||||
return FetchResult{}, err
|
||||
}
|
||||
clock.lap(&took.Fetch)
|
||||
res := FetchResult{Workspace: id, Commit: commit, Took: took}
|
||||
res := FetchResult{Workspace: id, Commit: commit}
|
||||
if base != "" {
|
||||
if err := m.merge(ctx, &res, tree, remote, base, commit, mergeOpts); err != nil {
|
||||
return FetchResult{}, err
|
||||
if err := m.merge(ctx, &res, tree, remote, base, commit); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
clock.lap(&took.Merge)
|
||||
if err := m.dissociate(ctx, tree); err != nil {
|
||||
return FetchResult{}, err
|
||||
return nil, err
|
||||
}
|
||||
clock.lap(&took.Dissociate)
|
||||
if res.Beside, err = m.placeSiblings(ctx, root, siblings); err != nil {
|
||||
return FetchResult{}, err
|
||||
return nil, err
|
||||
}
|
||||
clock.lap(&took.Beside)
|
||||
if (base == "" || res.Merge != "") && len(req.Paths) > 0 {
|
||||
if err := buildstep.Narrow(tree, req); err != nil {
|
||||
return FetchResult{}, err
|
||||
return nil, err
|
||||
}
|
||||
// Said beside the tree, so the steps that list the tree's go.mod files leave out those narrowed away.
|
||||
if err := os.WriteFile(filepath.Join(root, narrowedMark), nil, 0o600); err != nil {
|
||||
return FetchResult{}, err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
dirs, err := m.goModDirs(ctx, tree)
|
||||
if err != nil {
|
||||
return FetchResult{}, err
|
||||
return nil, err
|
||||
}
|
||||
res.Modules = dirs
|
||||
clock.lap(&took.Other)
|
||||
if base != "" && res.Merge == "" {
|
||||
return res, nil // a conflict: the tree is at the trunk, nothing of the head is built or tested
|
||||
}
|
||||
@@ -247,10 +168,9 @@ func (m *Module) fetchPhases(ctx context.Context, req buildstep.FetchRequest, id
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
clock.lap(&took.Modules)
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
return FetchResult{}, err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
@@ -373,7 +293,6 @@ func (m *Module) goEnv() []string {
|
||||
modcache = m.GoModCache
|
||||
}
|
||||
return []string{
|
||||
"CGO_ENABLED=0", // as a production build compiles (build.go), so a check tests what ships (issue 1921, S11)
|
||||
"GOTOOLCHAIN=local",
|
||||
"GOFLAGS=-mod=mod",
|
||||
"GOMODCACHE=" + modcache,
|
||||
|
||||
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -303,61 +302,3 @@ func TestWorkAndCacheAreTheDirectoriesTheManifestPlacesOutsideTheHome(t *testing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1921/S1: the fetch says how long each phase ran (mirror, fetch, merge, dissociate, beside, other, modules), and the
|
||||
// phases add up to its total, so a try's "clone" time is split into its parts.
|
||||
func TestTheFetchSaysTheTimeOfEachPhaseAndTheyAddUpToTheTotal(t *testing.T) {
|
||||
var mu sync.Mutex
|
||||
now := time.Date(2026, 10, 25, 9, 0, 0, 0, time.UTC)
|
||||
advance := func(s int) {
|
||||
mu.Lock()
|
||||
now = now.Add(time.Duration(s) * time.Second)
|
||||
mu.Unlock()
|
||||
}
|
||||
f := &fakeStep{gomods: "go.mod\n", files: map[string]string{"go.mod": "module x\n", "go.sum": ""}, rule: func(line string) (forgekey.Ran, bool) {
|
||||
switch {
|
||||
case strings.Contains(line, "/mirror/") && strings.Contains(line, " fetch "):
|
||||
advance(1)
|
||||
case strings.Contains(line, " fetch --no-tags --depth 1 "):
|
||||
advance(2)
|
||||
case strings.Contains(line, " merge --no-ff "):
|
||||
advance(3)
|
||||
case strings.Contains(line, " repack "):
|
||||
advance(4)
|
||||
case strings.HasSuffix(line, " mod download"):
|
||||
advance(5)
|
||||
}
|
||||
return forgekey.Ran{}, false
|
||||
}}
|
||||
m, states := fetchModule(t, f)
|
||||
said := []string{}
|
||||
m.Say = func(format string, a ...any) { said = append(said, fmt.Sprintf(format, a...)) }
|
||||
m.Now = func() time.Time { mu.Lock(); defer mu.Unlock(); return now }
|
||||
args := map[string]any{"repository": "novox/mesh-sdk", "commit": commitSHA, "base": strings.Repeat("b", 40)}
|
||||
if _, err := fetchTool(t, m)(args); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st := states.waitEnd(t)
|
||||
if st.State != delivery.StepPassed {
|
||||
t.Fatalf("the step ended %s: %s", st.State, st.Reason)
|
||||
}
|
||||
var res FetchResult
|
||||
if err := json.Unmarshal(st.Result, &res); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.Took == nil {
|
||||
t.Fatalf("the result says no phase times: %s", st.Result)
|
||||
}
|
||||
got := *res.Took
|
||||
// the mirror is brought to the head and to the base (1 s each), and the preview deepens from it (1 s) before merging
|
||||
want := FetchTook{Mirror: 2, Fetch: 2, Merge: 4, Dissociate: 4, Modules: 5, Total: 17}
|
||||
if got != want {
|
||||
t.Errorf("phases = %+v, want %+v", got, want)
|
||||
}
|
||||
if sum := got.Mirror + got.Fetch + got.Merge + got.Dissociate + got.Beside + got.Other + got.Modules; sum != got.Total {
|
||||
t.Errorf("the phases add up to %v, not the total %v", sum, got.Total)
|
||||
}
|
||||
if !strings.Contains(strings.Join(said, "\n"), "dissociate 4.0s") {
|
||||
t.Errorf("no log line names the phases: %q", said)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,8 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
||||
@@ -64,99 +62,34 @@ func (m *Module) integrationHere(ctx context.Context, workspace, tree string, pa
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
postgres, stopStore, err := m.startStore(ctx, run)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the integration level's store: %w", err)
|
||||
}
|
||||
defer stopStore()
|
||||
nats, stopBus, err := m.startBus(ctx, filepath.Join(run, "nats"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the integration level's bus: %w", err)
|
||||
}
|
||||
defer stopBus()
|
||||
env := append([]string{"MESH_TEST_POSTGRES=" + postgres, "MESH_TEST_NATS=" + nats}, m.besideEnv(workspace)...)
|
||||
// Compiling counts as setup: the tests are built once, here, with the addresses a TestMain may read, and the run
|
||||
// below finds them built. Whether they compile is for the run to say; its failure is not read here.
|
||||
// Only the directories the selected packages lie in compile (ADR 0536 §3); no selection is every directory.
|
||||
byDir, err := packagesByDir(tree, dirs, packages)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The tests the tags add, not the unit tests again (issue 1921, try speed S7): only the packages holding one compile
|
||||
// and run, with a -run pattern naming them. A try that names its own pattern runs it as asked.
|
||||
var runByDir map[string]string
|
||||
if pattern == "" {
|
||||
tagged, err := taggedTests(tree, dirs, packages, integrationTags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(tagged) == 0 {
|
||||
res := &TestResult{Level: LevelIntegration, Packages: []PackageResult{}}
|
||||
res.judge(m.integrationBounds())
|
||||
return res, nil // no test the tags add: nothing to start or run
|
||||
}
|
||||
byDir, runByDir, packages = map[string][]string{}, map[string]string{}, nil
|
||||
for d, tr := range tagged {
|
||||
byDir[d], runByDir[d] = tr.packages, tr.pattern
|
||||
packages = append(packages, tr.packages...)
|
||||
}
|
||||
sort.Strings(packages)
|
||||
}
|
||||
// The store, the bus and the compile start together (issue 1921, try speed S5); the setup is the longest of them.
|
||||
// The compile runs no test binary (-exec true), so it needs neither server: whether the tests compile is for the run
|
||||
// to say, and its failure is not read here; the run below finds them built.
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
postgres, nats string
|
||||
stopStore, stopBus func()
|
||||
storeErr, busErr error
|
||||
crash any
|
||||
crashMu sync.Mutex
|
||||
)
|
||||
guard := func() {
|
||||
if p := recover(); p != nil {
|
||||
crashMu.Lock()
|
||||
crash = p
|
||||
crashMu.Unlock()
|
||||
}
|
||||
}
|
||||
wg.Add(2)
|
||||
go func() { defer wg.Done(); defer guard(); postgres, stopStore, storeErr = m.startStore(ctx, run) }()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer guard()
|
||||
nats, stopBus, busErr = m.startBus(ctx, filepath.Join(run, "nats"))
|
||||
}()
|
||||
slots := m.slots(workspace) // shared with the ask's unit level (issue 1921, try speed S10)
|
||||
var slotErr error
|
||||
for _, d := range dirs {
|
||||
if len(byDir[d]) == 0 {
|
||||
continue
|
||||
}
|
||||
release, err := slots.acquire(ctx)
|
||||
if err != nil {
|
||||
slotErr = err
|
||||
break
|
||||
}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer release()
|
||||
defer guard()
|
||||
m.Run(ctx, m.goEnvFor(filepath.Join(tree, d)), "go", append([]string{"-C", filepath.Join(tree, d), "test", "-tags", strings.Join(integrationTags, ","), "-exec", "true", "-count=1"}, byDir[d]...)...)
|
||||
}()
|
||||
m.Run(ctx, append(m.goEnvFor(filepath.Join(tree, d)), env...), "go", append([]string{"-C", filepath.Join(tree, d), "test", "-tags", strings.Join(integrationTags, ","), "-run", "^$", "-count=1"}, byDir[d]...)...)
|
||||
}
|
||||
wg.Wait()
|
||||
if stopStore != nil {
|
||||
defer stopStore()
|
||||
}
|
||||
if stopBus != nil {
|
||||
defer stopBus()
|
||||
}
|
||||
if crash != nil {
|
||||
panic(crash)
|
||||
}
|
||||
if slotErr != nil {
|
||||
return nil, slotErr
|
||||
}
|
||||
if storeErr != nil {
|
||||
return nil, fmt.Errorf("the integration level's store: %w", storeErr)
|
||||
}
|
||||
if busErr != nil {
|
||||
return nil, fmt.Errorf("the integration level's bus: %w", busErr)
|
||||
}
|
||||
env := append([]string{"MESH_TEST_POSTGRES=" + postgres, "MESH_TEST_NATS=" + nats}, m.besideEnv(workspace)...)
|
||||
setup := m.clock().Sub(began)
|
||||
|
||||
return m.runLevel(ctx, tree, levelOpts{
|
||||
workspace: workspace, run: pattern, runByDir: runByDir,
|
||||
workspace: workspace, run: pattern,
|
||||
level: LevelIntegration, tags: integrationTags, packages: packages, bounds: m.integrationBounds(),
|
||||
env: env,
|
||||
setup: setup, wall: true,
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -36,8 +35,6 @@ type integFake struct {
|
||||
// command runs, with its line.
|
||||
gomods string
|
||||
onTest func(line string)
|
||||
// onCompile is called, unlocked, as each compile of the setup runs (issue 1921, try speed S5).
|
||||
onCompile func(line string)
|
||||
}
|
||||
|
||||
func newIntegFake(t *testing.T, captured string) *integFake {
|
||||
@@ -74,10 +71,7 @@ func (f *integFake) run(ctx context.Context, env []string, name string, args ...
|
||||
return forgekey.Ran{Stdout: f.gomods}
|
||||
}
|
||||
return forgekey.Ran{Stdout: "go.mod\n"}
|
||||
case name == "go" && strings.Contains(line, "-exec true"):
|
||||
if f.onCompile != nil {
|
||||
f.onCompile(line)
|
||||
}
|
||||
case name == "go" && strings.Contains(line, "-run ^$"):
|
||||
f.tick("compile")
|
||||
case name == "go":
|
||||
if f.onTest != nil {
|
||||
@@ -129,32 +123,9 @@ func integModule(t *testing.T, f *integFake) (*Module, *keptStates) {
|
||||
if err := os.MkdirAll(filepath.Join(m.Work, "ws1", "tree"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// A test the integration tag adds, so the level has a package to run (issue 1921, try speed S7).
|
||||
writeTaggedTest(t, filepath.Join(m.Work, "ws1", "tree"), "fixture/slow")
|
||||
return m, states
|
||||
}
|
||||
|
||||
// taggedTestFile is the file writeTaggedTest writes: one test the integration tag adds.
|
||||
const taggedTestFile = "level_integration_test.go"
|
||||
|
||||
// writeTaggedTest gives the go.mod directory dir the module path mp and one test the integration tag adds, in its root
|
||||
// package and in its package x.
|
||||
func writeTaggedTest(t *testing.T, dir, mp string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module "+mp+"\n\ngo 1.25.0\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := "//go:build integration\n\npackage x\n\nimport \"testing\"\n\nfunc TestTakesAWhile(t *testing.T) {}\n"
|
||||
for _, d := range []string{dir, filepath.Join(dir, "x")} {
|
||||
if err := os.MkdirAll(d, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(d, taggedTestFile), []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runLevelTool(t *testing.T, m *Module, states *keptStates, level string) (TestResult, delivery.Step) {
|
||||
t.Helper()
|
||||
for _, tool := range m.Tools(m.StepTools()) {
|
||||
@@ -343,11 +314,11 @@ func TestAStoreThatDoesNotStartEndsTheStepFailedAndLeavesNothing(t *testing.T) {
|
||||
if !strings.Contains(f.lines(), " stop") {
|
||||
t.Errorf("the half-started store was not stopped:\n%s", f.lines())
|
||||
}
|
||||
if f.busDir != "" && !f.busDown {
|
||||
t.Errorf("the bus, started beside the store, was not stopped after the store failed")
|
||||
if f.busDir != "" {
|
||||
t.Errorf("the bus was started after the store failed")
|
||||
}
|
||||
if left := workspaceLeft(filepath.Join(m.Work, "ws1")); len(left) != 1 || left[0] != "tree" {
|
||||
t.Errorf("the workspace holds %v, want only its tree", left)
|
||||
if entries, _ := os.ReadDir(filepath.Join(m.Work, "ws1")); len(entries) != 1 || entries[0].Name() != "tree" {
|
||||
t.Errorf("the workspace holds %v, want only its tree", entries)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,138 +377,3 @@ func TestTheSocketPathStaysUnderTheLimitForALongRunDirectory(t *testing.T) {
|
||||
t.Errorf("the short socket directory %s outlived the store (%v)", sock, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 1921/S5: the setup compiles the go.mod directories side by side, up to the bound, beside the store's and the bus's
|
||||
// start, and compiles without running a test binary, so it needs neither; the setup stays one number.
|
||||
func TestTheSetupCompilesTheDirectoriesAtOnceBesideTheStoreAndTheBus(t *testing.T) {
|
||||
f := newIntegFake(t, "slow")
|
||||
f.gomods = "a/go.mod\nb/go.mod\nc/go.mod\nd/go.mod\ne/go.mod\n"
|
||||
var mu sync.Mutex
|
||||
running, most := 0, 0
|
||||
storeStarting, storeUp, overlap := false, false, false
|
||||
f.onCompile = func(line string) {
|
||||
mu.Lock()
|
||||
running++
|
||||
most = max(most, running)
|
||||
mu.Unlock()
|
||||
time.Sleep(40 * time.Millisecond)
|
||||
mu.Lock()
|
||||
running--
|
||||
mu.Unlock()
|
||||
}
|
||||
m, states := integModule(t, f)
|
||||
m.Parallel = 3
|
||||
tree := filepath.Join(m.Work, "ws1", "tree")
|
||||
for _, d := range []string{"a", "b", "c", "d", "e"} {
|
||||
os.MkdirAll(filepath.Join(tree, d), 0o755)
|
||||
writeTaggedTest(t, filepath.Join(tree, d), "x/"+d)
|
||||
}
|
||||
inner := m.Run
|
||||
m.Run = func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
|
||||
line := strings.Join(args, " ")
|
||||
if name == "pg_ctl" && strings.HasSuffix(line, " start") {
|
||||
mu.Lock()
|
||||
storeStarting = true
|
||||
mu.Unlock()
|
||||
time.Sleep(100 * time.Millisecond) // the store takes a while to start
|
||||
defer func() { mu.Lock(); storeUp = true; mu.Unlock() }()
|
||||
}
|
||||
if name == "go" && strings.Contains(line, "-exec true") {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
mu.Lock()
|
||||
overlap = overlap || (storeStarting && !storeUp)
|
||||
mu.Unlock()
|
||||
for _, e := range env {
|
||||
if strings.HasPrefix(e, "MESH_TEST_") {
|
||||
t.Errorf("the compile was given %s: it runs no test binary", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
return inner(ctx, env, name, args...)
|
||||
}
|
||||
res, st := runLevelTool(t, m, states, "integration")
|
||||
if st.State != delivery.StepPassed {
|
||||
t.Fatalf("the step ended %s: %s", st.State, st.Reason)
|
||||
}
|
||||
if most != 3 {
|
||||
t.Errorf("%d compiles ran at once, want the bound of 3", most)
|
||||
}
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if !overlap {
|
||||
t.Errorf("no compile ran while the store was starting:\n%s", f.lines())
|
||||
}
|
||||
if res.Setup < 0 {
|
||||
t.Errorf("setup = %v", res.Setup)
|
||||
}
|
||||
}
|
||||
|
||||
// 1921/S7: the integration level runs the tests its tags add, not the unit tests again: only the packages holding a
|
||||
// tagged test compile and run, with a -run pattern naming those tests; a tree with no tagged test starts no store.
|
||||
func TestTheIntegrationLevelRunsOnlyTheTestsItsTagsAdd(t *testing.T) {
|
||||
f := newIntegFake(t, "slow")
|
||||
m, states := integModule(t, f)
|
||||
tree := filepath.Join(m.Work, "ws1", "tree")
|
||||
os.RemoveAll(filepath.Join(tree, "x"))
|
||||
os.Remove(filepath.Join(tree, taggedTestFile))
|
||||
for name, body := range map[string]string{
|
||||
"go.mod": "module fixture/slow\n",
|
||||
"unit/u_test.go": "package unit\n\nimport \"testing\"\n\nfunc TestUnit(t *testing.T) {}\n",
|
||||
"store/s_test.go": "package store\n\nimport \"testing\"\n\nfunc TestPlain(t *testing.T) {}\n",
|
||||
"store/s_integration_test.go": "//go:build integration\n\npackage store\n\nimport \"testing\"\n\nfunc TestMain(m *testing.M) {}\nfunc TestReal(t *testing.T) {}\nfunc Testable() {}\nfunc helper() {}\n",
|
||||
"store/fake_test.go": "//go:build !integration\n\npackage store\n\nimport \"testing\"\n\nfunc TestFake(t *testing.T) {}\n",
|
||||
"iso/iso_test.go": "//go:build integration && isolation && linux\n\npackage iso\n\nimport \"testing\"\n\nfunc TestShutOff(t *testing.T) {}\n",
|
||||
"plat/p_linux_test.go": "//go:build linux\n\npackage plat\n\nimport \"testing\"\n\nfunc TestLinux(t *testing.T) {}\n",
|
||||
} {
|
||||
os.MkdirAll(filepath.Dir(filepath.Join(tree, name)), 0o755)
|
||||
if err := os.WriteFile(filepath.Join(tree, name), []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if _, st := runLevelTool(t, m, states, "integration"); st.State != delivery.StepPassed {
|
||||
t.Fatalf("the step ended %s: %s", st.State, st.Reason)
|
||||
}
|
||||
var compiled, ran []string
|
||||
for _, c := range f.calls {
|
||||
switch {
|
||||
case strings.HasPrefix(c.line, "go ") && strings.Contains(c.line, "-exec true"):
|
||||
compiled = append(compiled, c.line)
|
||||
case strings.HasPrefix(c.line, "go ") && strings.Contains(c.line, " -json"):
|
||||
ran = append(ran, c.line)
|
||||
}
|
||||
}
|
||||
if len(ran) != 1 || !strings.Contains(ran[0], "-run ^(TestReal|TestShutOff)$") ||
|
||||
!strings.HasSuffix(ran[0], " fixture/slow/iso fixture/slow/store") {
|
||||
t.Errorf("the run = %q, want the tagged tests of iso and store alone", ran)
|
||||
}
|
||||
if len(compiled) != 1 || !strings.HasSuffix(compiled[0], " fixture/slow/iso fixture/slow/store") {
|
||||
t.Errorf("the compile = %q, want iso and store alone", compiled)
|
||||
}
|
||||
|
||||
g := newIntegFake(t, "slow")
|
||||
m, states = integModule(t, g)
|
||||
tree = filepath.Join(m.Work, "ws1", "tree")
|
||||
os.RemoveAll(filepath.Join(tree, "x"))
|
||||
os.Remove(filepath.Join(tree, taggedTestFile))
|
||||
os.WriteFile(filepath.Join(tree, "u_test.go"), []byte("package x\n\nimport \"testing\"\n\nfunc TestUnit(t *testing.T) {}\n"), 0o644)
|
||||
res, st := runLevelTool(t, m, states, "integration")
|
||||
if st.State != delivery.StepPassed || !res.Passed || !res.NoTests {
|
||||
t.Fatalf("a tree with no tagged test: %s %+v", st.State, res)
|
||||
}
|
||||
if strings.Contains(g.lines(), "pg_ctl") || strings.Contains(g.lines(), "go -C") {
|
||||
t.Errorf("a tree with no tagged test started the store or ran go:\n%s", g.lines())
|
||||
}
|
||||
}
|
||||
|
||||
// workspaceLeft is what a workspace holds besides the directories its ask keeps there (askDirs: its cache layer and its
|
||||
// bound on processes, gone with the workspace; issue 1921, S2 and S10).
|
||||
func workspaceLeft(dir string) []string {
|
||||
var left []string
|
||||
entries, _ := os.ReadDir(dir)
|
||||
for _, e := range entries {
|
||||
if !slices.Contains(askDirs, e.Name()) {
|
||||
left = append(left, e.Name())
|
||||
}
|
||||
}
|
||||
return left
|
||||
}
|
||||
|
||||
@@ -29,30 +29,21 @@ func (m *Module) caches() []string {
|
||||
}
|
||||
|
||||
// isolation is how a run step of the workspace is shut off: the workspace (its tree, its siblings, its run directories,
|
||||
// its packed artifacts) read-write, the caches beneath the ask's own layer unless write, and an environment of a PATH and
|
||||
// its packed artifacts) read-write, the caches beneath a throwaway layer unless write, and an environment of a PATH and
|
||||
// a locale alone (the step's HOME is its own, inside its /tmp).
|
||||
func (m *Module) isolation(workspace string, write bool) buildstep.Isolation {
|
||||
path := os.Getenv("PATH")
|
||||
if path == "" {
|
||||
path = defaultPath
|
||||
}
|
||||
iso := buildstep.Isolation{
|
||||
return buildstep.Isolation{
|
||||
Workspace: filepath.Join(m.Work, workspace),
|
||||
Caches: m.caches(),
|
||||
WriteCaches: write,
|
||||
Env: []string{"PATH=" + path, "LC_ALL=C"},
|
||||
}
|
||||
if !write {
|
||||
// What one command of the ask compiles is kept for its later commands (the vet, the unit tests, the integration
|
||||
// level's compile), in the workspace and gone with it (issue 1921, try speed S2).
|
||||
iso.CacheLayer = filepath.Join(iso.Workspace, cacheLayerDir)
|
||||
}
|
||||
return iso
|
||||
}
|
||||
|
||||
// cacheLayerDir is the directory in a workspace's root where an ask keeps what its commands wrote into the caches.
|
||||
const cacheLayerDir = "cachelayer"
|
||||
|
||||
// stepRunner is the runner of a run step of the workspace: buildstep.Isolated over m.Run, so every command it runs is
|
||||
// shut off; write is true for a trunk build alone (the shared caches are written by trunk builds only). Inside is the
|
||||
// integration level's own process, already running shut off (level.go): it runs its commands as they are.
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
type shutOff struct {
|
||||
flags []string
|
||||
binds, roBinds, overlays []string
|
||||
uppers map[string]string // the writable layer each --overlay gives a cache: cache -> upper
|
||||
env []string
|
||||
command string
|
||||
chdir string
|
||||
@@ -28,7 +27,7 @@ type shutOff struct {
|
||||
|
||||
// bwrapArity is how many words each bubblewrap option the SDK writes takes.
|
||||
var bwrapArity = map[string]int{"--unshare-all": 0, "--die-with-parent": 0, "--new-session": 0, "--clearenv": 0,
|
||||
"--proc": 1, "--dev": 1, "--tmpfs": 1, "--dir": 1, "--chdir": 1, "--overlay-src": 1, "--tmp-overlay": 1, "--overlay": 3,
|
||||
"--proc": 1, "--dev": 1, "--tmpfs": 1, "--dir": 1, "--chdir": 1, "--overlay-src": 1, "--tmp-overlay": 1,
|
||||
"--ro-bind": 2, "--ro-bind-try": 2, "--bind": 2, "--symlink": 2, "--setenv": 2}
|
||||
|
||||
// throughBubblewrap is the fake runner of a step's tests: a bubblewrap command is read back (recorded in seen) and its
|
||||
@@ -57,11 +56,6 @@ func throughBubblewrap(t *testing.T, run forgekey.Runner, seen *[]shutOff) forge
|
||||
s.roBinds = append(s.roBinds, args[i+1])
|
||||
case "--overlay-src":
|
||||
s.overlays = append(s.overlays, args[i+1])
|
||||
case "--overlay":
|
||||
if s.uppers == nil {
|
||||
s.uppers = map[string]string{}
|
||||
}
|
||||
s.uppers[args[i+3]] = args[i+1]
|
||||
case "--setenv":
|
||||
s.env = append(s.env, args[i+1]+"="+args[i+2])
|
||||
case "--chdir":
|
||||
@@ -111,9 +105,6 @@ func checkShutOff(t *testing.T, m *Module, s shutOff, write bool) {
|
||||
t.Errorf("a trunk build's %s does not write the shared cache %s (bound %v, layered %v)", s.command, c, bound, layered)
|
||||
case !write && (bound || !layered):
|
||||
t.Errorf("%s may write the shared cache %s (bound %v, layered %v): only a trunk build does", s.command, c, bound, layered)
|
||||
case !write && !strings.HasPrefix(s.uppers[c], filepath.Join(m.Work, "ws1", "cachelayer")+"/"):
|
||||
// 1921/S2: what one command of the ask compiles is kept for its later commands, in the workspace
|
||||
t.Errorf("%s writes the cache %s into %q, not the ask's cache layer in its workspace", s.command, c, s.uppers[c])
|
||||
}
|
||||
}
|
||||
if want := filepath.Join(m.Work, "ws1"); !slices.Contains(s.binds, want) {
|
||||
@@ -140,8 +131,7 @@ func TestTheUnitLevelRunsEveryGoTestShutOff(t *testing.T) {
|
||||
t.Fatalf("%d go commands ran, %d of them shut off: %v", ran, len(seen), *calls)
|
||||
}
|
||||
for _, s := range seen {
|
||||
// the lint's go vet, a compile alone, writes the shared cache (issue 1921, S11): cgo_test.go
|
||||
checkShutOff(t, m, s, strings.Contains(s.command, " vet "))
|
||||
checkShutOff(t, m, s, false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,8 +41,6 @@ type levelSpec struct {
|
||||
Run string `json:"run,omitempty"`
|
||||
Setup time.Duration `json:"setup"`
|
||||
Bound time.Duration `json:"bound"`
|
||||
// Parallel is the ask's bound on processes, which the process shares with the ask's other level (issue 1921, S10).
|
||||
Parallel int `json:"parallel,omitempty"`
|
||||
}
|
||||
|
||||
// levelAnswer is the one line the process prints: the level's result or the digest, or why it could not run.
|
||||
@@ -85,7 +83,7 @@ func (m *Module) integrationShutOff(ctx context.Context, workspace string, packa
|
||||
// workspace read-write, the program alone read-only, the shared caches beneath a throwaway layer, the Node build
|
||||
// module's toolchains read-only for the integration level.
|
||||
func (m *Module) shutOff(ctx context.Context, exe string, spec levelSpec) (levelAnswer, error) {
|
||||
spec.Work, spec.Cache, spec.Parallel = m.Work, m.Cache, m.parallel()
|
||||
spec.Work, spec.Cache = m.Work, m.Cache
|
||||
raw, err := json.Marshal(spec)
|
||||
if err != nil {
|
||||
return levelAnswer{}, err
|
||||
@@ -167,7 +165,7 @@ func runLevelProcess(args []string) int {
|
||||
return say(levelAnswer{Error: fmt.Sprintf("%q is not a workspace id", spec.Workspace)})
|
||||
}
|
||||
m := &Module{Inside: true, Recipe: Current(), Work: spec.Work, Cache: spec.Cache, Home: os.Getenv("HOME"), UnitBound: spec.Bound,
|
||||
IntegrationBounds: Bounds{Setup: spec.Setup, Run: spec.Bound}, Parallel: spec.Parallel, Say: func(string, ...any) {}}
|
||||
IntegrationBounds: Bounds{Setup: spec.Setup, Run: spec.Bound}, Say: func(string, ...any) {}}
|
||||
m.Run = StepRunner(m.Home)
|
||||
return say(m.answer(context.Background(), spec))
|
||||
}
|
||||
|
||||
@@ -57,14 +57,8 @@ type Failure struct {
|
||||
Test string `json:"test,omitempty"`
|
||||
Output string `json:"output,omitempty"`
|
||||
Cause string `json:"cause,omitempty"`
|
||||
// Step is StepLint for a finding of the lint step (gofmt, go vet), which runs beside the unit level and is not one of
|
||||
// the levels (ADR 0542 §2): the checker reports it as the lint step's failure. Empty for a test's failure.
|
||||
Step string `json:"step,omitempty"`
|
||||
}
|
||||
|
||||
// StepLint is the Step of a lint finding.
|
||||
const StepLint = "lint"
|
||||
|
||||
// TestResult is what a test step passes with.
|
||||
type TestResult struct {
|
||||
Level string `json:"level"`
|
||||
@@ -79,7 +73,7 @@ type TestResult struct {
|
||||
Elapsed float64 `json:"elapsed"`
|
||||
// Setup is the level's setup time in seconds, when it has a setup.
|
||||
Setup float64 `json:"setup,omitempty"`
|
||||
// Lint is the unit level's gofmt and go vet time in seconds, run beside its tests (ADR 0529 §1; issue 1921).
|
||||
// Lint is the unit level's gofmt and go vet time in seconds, before its tests (ADR 0529 §1).
|
||||
Lint float64 `json:"lint,omitempty"`
|
||||
// Bound and SetupBound are the bounds the level was judged by, in seconds.
|
||||
Bound float64 `json:"bound"`
|
||||
|
||||
@@ -60,23 +60,17 @@ func lintable(dir string, each func(path string) error) error {
|
||||
})
|
||||
}
|
||||
|
||||
// unformatted are the Go files of a directory gofmt would change, relative to the tree, the build tags the directory's
|
||||
// files name, and whether a file is left out by one of them (a negated tag, //go:build !integration). Only files whose
|
||||
// directory (relative to the go.mod directory, "." for its own) is in only are checked for gofmt; nil checks every file.
|
||||
func unformatted(tree, d string, only map[string]bool) (files, tags []string, negated bool, err error) {
|
||||
// unformatted are the Go files of a directory gofmt would change, relative to the tree, and the build tags they name.
|
||||
func unformatted(tree, d string) (files, tags []string, err error) {
|
||||
seen := map[string]bool{}
|
||||
base := filepath.Join(tree, d)
|
||||
err = lintable(base, func(path string) error {
|
||||
err = lintable(filepath.Join(tree, d), func(path string) error {
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pkgDir, _ := filepath.Rel(base, filepath.Dir(path))
|
||||
if only == nil || only[filepath.ToSlash(pkgDir)] {
|
||||
if out, ferr := format.Source(src); ferr != nil || !bytes.Equal(out, src) {
|
||||
rel, _ := filepath.Rel(tree, path)
|
||||
files = append(files, filepath.ToSlash(rel))
|
||||
}
|
||||
if out, ferr := format.Source(src); ferr != nil || !bytes.Equal(out, src) {
|
||||
rel, _ := filepath.Rel(tree, path)
|
||||
files = append(files, filepath.ToSlash(rel))
|
||||
}
|
||||
scan := bufio.NewScanner(bytes.NewReader(src))
|
||||
for scan.Scan() {
|
||||
@@ -85,15 +79,8 @@ func unformatted(tree, d string, only map[string]bool) (files, tags []string, ne
|
||||
break
|
||||
}
|
||||
if expr, ok := strings.CutPrefix(line, "//go:build "); ok {
|
||||
for _, at := range buildTag.FindAllStringIndex(expr, -1) {
|
||||
tag := expr[at[0]:at[1]]
|
||||
if notATag[tag] || strings.HasPrefix(tag, "go1") {
|
||||
continue
|
||||
}
|
||||
if before := strings.TrimRight(expr[:at[0]], " ("); strings.HasSuffix(before, "!") {
|
||||
negated = true
|
||||
}
|
||||
if !seen[tag] {
|
||||
for _, tag := range buildTag.FindAllString(expr, -1) {
|
||||
if !notATag[tag] && !strings.HasPrefix(tag, "go1") && !seen[tag] {
|
||||
seen[tag] = true
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
@@ -104,123 +91,62 @@ func unformatted(tree, d string, only map[string]bool) (files, tags []string, ne
|
||||
})
|
||||
sort.Strings(files)
|
||||
sort.Strings(tags)
|
||||
return files, tags, negated, err
|
||||
return files, tags, err
|
||||
}
|
||||
|
||||
// boundOf is a bound of n processes of this process alone.
|
||||
func boundOf(n int) func(context.Context) (func(), error) {
|
||||
slots := make(chan struct{}, n)
|
||||
return func(context.Context) (func(), error) {
|
||||
slots <- struct{}{}
|
||||
return func() { <-slots }, nil
|
||||
}
|
||||
}
|
||||
|
||||
// lintScope is what of a go.mod directory d the lint covers, from the patterns the level runs there: every package
|
||||
// (./..., and the directories nil) when a pattern is relative or none is given, else the import paths given and the
|
||||
// directories of their files, relative to d.
|
||||
func lintScope(tree, d string, patterns []string) (targets []string, dirs map[string]bool) {
|
||||
if len(patterns) == 0 {
|
||||
return []string{"./..."}, nil
|
||||
}
|
||||
raw, _ := os.ReadFile(filepath.Join(tree, d, "go.mod"))
|
||||
mp := modulePath(string(raw))
|
||||
dirs = map[string]bool{}
|
||||
for _, p := range patterns {
|
||||
if strings.HasPrefix(p, "./") || p == "." {
|
||||
return []string{"./..."}, nil
|
||||
}
|
||||
rel, ok := strings.CutPrefix(p, mp)
|
||||
if mp == "" || !ok || (rel != "" && !strings.HasPrefix(rel, "/")) {
|
||||
return []string{"./..."}, nil // a package of no module here: the whole directory, as before
|
||||
}
|
||||
if rel = strings.TrimPrefix(rel, "/"); rel == "" {
|
||||
rel = "."
|
||||
}
|
||||
dirs[rel] = true
|
||||
targets = append(targets, p)
|
||||
}
|
||||
return targets, dirs
|
||||
}
|
||||
|
||||
// lint is gofmt and go vet of one go.mod directory d of the tree, run with run in the directory's go environment, over
|
||||
// the packages the level runs there (patterns, as packagesByDir gives them; issue 1921, try speed S3); a failure of either
|
||||
// is a Failure of the level naming what failed. go vet runs once with every tag the directory names, which vets the
|
||||
// untagged files too; only a directory where a tag leaves a file out vets also without tags, both passes at once.
|
||||
func (m *Module) lint(ctx context.Context, run forgekey.Runner, tree, d string, env, patterns []string) ([]Failure, error) {
|
||||
targets, only := lintScope(tree, d, patterns)
|
||||
files, tags, negated, err := unformatted(tree, d, only)
|
||||
// lint is gofmt and go vet of one go.mod directory d of the tree, run with run in the directory's go environment; a
|
||||
// failure of either is a Failure of the level naming what failed.
|
||||
func (m *Module) lint(ctx context.Context, run forgekey.Runner, tree, d string, env []string) ([]Failure, error) {
|
||||
files, tags, err := unformatted(tree, d)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading the Go files of %s: %w", d, err)
|
||||
}
|
||||
var failed []Failure
|
||||
if len(files) > 0 {
|
||||
failed = append(failed, Failure{Package: d, Test: "gofmt", Step: StepLint,
|
||||
failed = append(failed, Failure{Package: d, Test: "gofmt",
|
||||
Output: "not as gofmt writes them (run gofmt -w): " + strings.Join(files, ", ")})
|
||||
}
|
||||
sets := [][]string{tags}
|
||||
if len(tags) > 0 && negated {
|
||||
sets = [][]string{nil, tags}
|
||||
sets := [][]string{nil}
|
||||
if len(tags) > 0 {
|
||||
sets = append(sets, tags)
|
||||
}
|
||||
type vetRun struct {
|
||||
name string
|
||||
r forgekey.Ran
|
||||
}
|
||||
runs := make([]vetRun, len(sets))
|
||||
var wg sync.WaitGroup
|
||||
for i, set := range sets {
|
||||
for _, set := range sets {
|
||||
args := []string{"-C", filepath.Join(tree, d), "vet"}
|
||||
name := "go vet"
|
||||
if len(set) > 0 {
|
||||
args = append(args, "-tags", strings.Join(set, ","))
|
||||
name += " -tags " + strings.Join(set, ",")
|
||||
}
|
||||
args = append(args, targets...)
|
||||
runs[i].name = name
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
runs[i].r = run(ctx, env, "go", args...)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
for _, v := range runs {
|
||||
if v.r.Err != "" {
|
||||
return nil, fmt.Errorf("%s in %s could not be run: %s", v.name, d, firstLine(v.r.Err+"\n"+v.r.Stderr))
|
||||
args = append(args, "./...")
|
||||
r := run(ctx, env, "go", args...)
|
||||
if r.Err != "" {
|
||||
return nil, fmt.Errorf("%s in %s could not be run: %s", name, d, firstLine(r.Err+"\n"+r.Stderr))
|
||||
}
|
||||
if v.r.Status != 0 {
|
||||
failed = append(failed, Failure{Package: d, Test: v.name, Step: StepLint, Output: strings.TrimSpace(v.r.Stderr + "\n" + v.r.Stdout)})
|
||||
if r.Status != 0 {
|
||||
failed = append(failed, Failure{Package: d, Test: name, Output: strings.TrimSpace(r.Stderr + "\n" + r.Stdout)})
|
||||
}
|
||||
}
|
||||
return failed, nil
|
||||
}
|
||||
|
||||
// lintAll lints each directory, up to m.parallel() at once like the level's tests; the failures join in directory order
|
||||
// and the error is the first directory's, so the answer does not depend on which ended first (issue 1930). byDir names
|
||||
// the packages the level runs in each directory (packagesByDir); nil lints each directory whole. acquire takes a slot of
|
||||
// the bound the lint shares with the ask's tests; nil gives it one of its own.
|
||||
func (m *Module) lintAll(ctx context.Context, run forgekey.Runner, tree string, dirs []string, env func(d string) []string, byDir map[string][]string, acquire func(context.Context) (func(), error)) ([]Failure, error) {
|
||||
// and the error is the first directory's, so the answer does not depend on which ended first (issue 1930).
|
||||
func (m *Module) lintAll(ctx context.Context, run forgekey.Runner, tree string, dirs []string, env func(d string) []string) ([]Failure, error) {
|
||||
type answer struct {
|
||||
failed []Failure
|
||||
err error
|
||||
}
|
||||
answers := make([]answer, len(dirs))
|
||||
if acquire == nil {
|
||||
acquire = boundOf(m.parallel())
|
||||
}
|
||||
slots := make(chan struct{}, m.parallel())
|
||||
var wg sync.WaitGroup
|
||||
var crash any // a panic of a lint goes on in the caller, as a panic of a test run does
|
||||
var crashMu sync.Mutex
|
||||
for i, d := range dirs {
|
||||
release, err := acquire(ctx)
|
||||
if err != nil {
|
||||
answers[i].err = err
|
||||
break
|
||||
}
|
||||
wg.Add(1)
|
||||
slots <- struct{}{}
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer release()
|
||||
defer func() { <-slots }()
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
crashMu.Lock()
|
||||
@@ -228,7 +154,7 @@ func (m *Module) lintAll(ctx context.Context, run forgekey.Runner, tree string,
|
||||
crashMu.Unlock()
|
||||
}
|
||||
}()
|
||||
answers[i].failed, answers[i].err = m.lint(ctx, run, tree, d, env(d), byDir[d])
|
||||
answers[i].failed, answers[i].err = m.lint(ctx, run, tree, d, env(d))
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
@@ -4,13 +4,11 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
)
|
||||
|
||||
@@ -30,9 +28,8 @@ func writeTree(t *testing.T, files map[string]string) string {
|
||||
}
|
||||
|
||||
// 1887/D7-go, ADR 0529 §1: an unformatted file fails the unit level naming the file (testdata and vendor are left
|
||||
// alone), and go vet runs with every tag the files name together, integration and isolation included, so a vet error
|
||||
// behind isolation fails the level naming the run. One pass with every tag vets the untagged files too (issue 1921, try
|
||||
// speed S3), as no file here is left out by a tag.
|
||||
// alone), and go vet runs with no tag and with every tag the files name together, integration and isolation included, so
|
||||
// a vet error behind isolation fails the level naming the run.
|
||||
func TestTheUnitLevelRunsGofmtAndVetWithEveryTag(t *testing.T) {
|
||||
tree := writeTree(t, map[string]string{
|
||||
"go.mod": "module x\n\ngo 1.26\n",
|
||||
@@ -54,11 +51,11 @@ func TestTheUnitLevelRunsGofmtAndVetWithEveryTag(t *testing.T) {
|
||||
}
|
||||
return forgekey.Ran{}
|
||||
}
|
||||
failed, err := (&Module{}).lint(context.Background(), run, tree, ".", nil, nil)
|
||||
failed, err := (&Module{}).lint(context.Background(), run, tree, ".", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Join(vetted, " | ") != "vet -tags integration,isolation ./..." {
|
||||
if strings.Join(vetted, " | ") != "vet ./... | vet -tags integration,isolation ./..." {
|
||||
t.Errorf("vet ran %v", vetted)
|
||||
}
|
||||
if len(failed) != 2 || failed[0].Test != "gofmt" || !strings.Contains(failed[0].Output, "b.go") ||
|
||||
@@ -70,14 +67,14 @@ func TestTheUnitLevelRunsGofmtAndVetWithEveryTag(t *testing.T) {
|
||||
}
|
||||
// A formatted directory that vets passes.
|
||||
clean := writeTree(t, map[string]string{"go.mod": "module y\n", "a.go": "package y\n"})
|
||||
if failed, err := (&Module{}).lint(context.Background(), func(context.Context, []string, string, ...string) forgekey.Ran { return forgekey.Ran{} }, clean, ".", nil, nil); err != nil || len(failed) != 0 {
|
||||
if failed, err := (&Module{}).lint(context.Background(), func(context.Context, []string, string, ...string) forgekey.Ran { return forgekey.Ran{} }, clean, ".", nil); err != nil || len(failed) != 0 {
|
||||
t.Errorf("a clean directory: %+v, %v", failed, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 1887/D7-go: through the step, the unit level of a tree holding an unformatted file fails, naming the file.
|
||||
func TestAnUnformattedFileFailsTheUnitLevel(t *testing.T) {
|
||||
m, states, ws, _ := workspaceFor(t, "empty")
|
||||
m, states, ws, calls := workspaceFor(t, "empty")
|
||||
tree := filepath.Join(m.Work, ws, "tree")
|
||||
for name, body := range map[string]string{"go.mod": "module x\n", "x.go": "package x\nfunc X() {}\n"} {
|
||||
if err := os.WriteFile(filepath.Join(tree, name), []byte(body), 0o644); err != nil {
|
||||
@@ -88,117 +85,50 @@ func TestAnUnformattedFileFailsTheUnitLevel(t *testing.T) {
|
||||
if res.Passed || len(res.Failed) != 1 || res.Failed[0].Test != "gofmt" || !strings.Contains(res.Failed[0].Output, "x.go") {
|
||||
t.Fatalf("result = %+v, want the level failed naming x.go", res)
|
||||
}
|
||||
}
|
||||
|
||||
// 1921/S4: the lint runs beside the unit level's tests, not before them: both have started before either ends, a lint
|
||||
// failure still fails the level, and the result reports the lint's time and the tests' failures both.
|
||||
func TestTheLintRunsBesideTheUnitTestsAndItsFailureStillFailsTheLevel(t *testing.T) {
|
||||
m, states, ws, _ := workspaceFor(t, "failing")
|
||||
tree := filepath.Join(m.Work, ws, "tree")
|
||||
for name, body := range map[string]string{"go.mod": "module fixture/failing\n", "x.go": "package x\nfunc X() {}\n"} {
|
||||
if err := os.WriteFile(filepath.Join(tree, name), []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
// Review round 2 item 5: before the level's tests, which do not run once gofmt failed.
|
||||
for _, c := range *calls {
|
||||
if strings.Contains(c.line, " test -json") {
|
||||
t.Errorf("the tests ran after gofmt failed: %s", c.line)
|
||||
}
|
||||
}
|
||||
m.Parallel = 4
|
||||
inner := m.Run
|
||||
var vetStarted, testStarted sync.Once
|
||||
vet, test := make(chan struct{}), make(chan struct{})
|
||||
timeout := time.After(2 * time.Second)
|
||||
m.Run = func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
|
||||
line := strings.Join(args, " ")
|
||||
switch {
|
||||
case strings.Contains(line, " vet "):
|
||||
vetStarted.Do(func() { close(vet) })
|
||||
select { // the vet ends only once a test has started
|
||||
case <-test:
|
||||
case <-timeout:
|
||||
t.Error("no test started while the vet ran")
|
||||
}
|
||||
case strings.Contains(line, " test -json"):
|
||||
testStarted.Do(func() { close(test) })
|
||||
select {
|
||||
case <-vet:
|
||||
case <-timeout:
|
||||
t.Error("no vet started while the tests ran")
|
||||
}
|
||||
}
|
||||
return inner(ctx, env, name, args...)
|
||||
}
|
||||
res, st := runUnit(t, m, states, ws)
|
||||
if st.State != delivery.StepPassed {
|
||||
t.Fatalf("the step ended %s: %s", st.State, st.Reason)
|
||||
}
|
||||
if res.Passed || len(res.Failed) < 2 || res.Failed[0].Test != "gofmt" || res.Failed[1].Test != "TestThreeIsFour" {
|
||||
t.Fatalf("failed = %+v, want gofmt's failure, then the test's", res.Failed)
|
||||
}
|
||||
// ADR 0542 §2: the lint's failure is the lint step's, not the unit level's.
|
||||
if res.Failed[0].Step != StepLint || res.Failed[1].Step != "" {
|
||||
t.Errorf("steps = %q, %q; want the gofmt failure the lint step's and the test's the level's", res.Failed[0].Step, res.Failed[1].Step)
|
||||
}
|
||||
}
|
||||
|
||||
// 1921/S3: the lint of a level that runs some packages vets those packages alone, in one pass with every tag their
|
||||
// directory names, and reports a gofmt fault only in their files: a fault in a package the level keeps was reported
|
||||
// when that package changed.
|
||||
func TestTheLintOfSomePackagesVetsThemInOnePassAndFormatsOnlyTheirFiles(t *testing.T) {
|
||||
tree := writeTree(t, map[string]string{
|
||||
"go.mod": "module x\n\ngo 1.26\n",
|
||||
"a/a.go": "package a\n\nfunc A() {}\n",
|
||||
"a/a_integration_test.go": "//go:build integration\n\npackage a\n",
|
||||
"b/b.go": "package b\nfunc B() {}\n",
|
||||
"a/deeper/d.go": "package deeper\nfunc D() {}\n",
|
||||
})
|
||||
var vetted []string
|
||||
run := func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
|
||||
vetted = append(vetted, strings.Join(args[2:], " "))
|
||||
return forgekey.Ran{}
|
||||
// Issue 1930: the lint of many directories runs side by side up to the level's bound, not one after another, and the
|
||||
// failures still join in directory order.
|
||||
func TestTheLintOfManyDirectoriesRunsAtOnceUpToTheBound(t *testing.T) {
|
||||
files := map[string]string{}
|
||||
var dirs []string
|
||||
for _, d := range []string{"a", "b", "c", "d", "e", "f"} {
|
||||
files[d+"/go.mod"] = "module x/" + d + "\n"
|
||||
files[d+"/x.go"] = "package x\n"
|
||||
dirs = append(dirs, d)
|
||||
}
|
||||
failed, err := (&Module{}).lint(context.Background(), run, tree, ".", nil, []string{"x/a"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Join(vetted, " | ") != "vet -tags integration x/a" {
|
||||
t.Errorf("vet ran %v, want one pass over x/a with its tag", vetted)
|
||||
}
|
||||
if len(failed) != 0 {
|
||||
t.Errorf("the faults of packages the level keeps were reported: %+v", failed)
|
||||
}
|
||||
vetted = nil
|
||||
failed, err = (&Module{}).lint(context.Background(), run, tree, ".", nil, []string{"x/b"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(failed) != 1 || failed[0].Test != "gofmt" || !strings.Contains(failed[0].Output, "b/b.go") || strings.Contains(failed[0].Output, "d.go") {
|
||||
t.Errorf("failed = %+v, want gofmt naming b/b.go alone", failed)
|
||||
}
|
||||
if strings.Join(vetted, " | ") != "vet -tags integration x/b" {
|
||||
t.Errorf("vet ran %v", vetted)
|
||||
}
|
||||
}
|
||||
|
||||
// 1921/S3: a file a tag leaves out (//go:build !integration) is vetted by no pass with the tag, so a directory holding
|
||||
// one keeps the pass without tags beside the pass with them.
|
||||
func TestADirectoryWithAFileATagLeavesOutKeepsThePassWithoutTags(t *testing.T) {
|
||||
tree := writeTree(t, map[string]string{
|
||||
"go.mod": "module x\n",
|
||||
"a.go": "package x\n",
|
||||
"fake.go": "//go:build !integration\n\npackage x\n",
|
||||
"real_test.go": "//go:build integration\n\npackage x\n",
|
||||
})
|
||||
tree := writeTree(t, files)
|
||||
var mu sync.Mutex
|
||||
var vetted []string
|
||||
running, most := 0, 0
|
||||
run := func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
|
||||
mu.Lock()
|
||||
vetted = append(vetted, strings.Join(args[2:], " "))
|
||||
running++
|
||||
most = max(most, running)
|
||||
mu.Unlock()
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
mu.Lock()
|
||||
running--
|
||||
mu.Unlock()
|
||||
if strings.HasSuffix(args[1], "b") || strings.HasSuffix(args[1], "e") {
|
||||
return forgekey.Ran{Status: 1, Stderr: "bad " + filepath.Base(args[1])}
|
||||
}
|
||||
return forgekey.Ran{}
|
||||
}
|
||||
if _, err := (&Module{}).lint(context.Background(), run, tree, ".", nil, nil); err != nil {
|
||||
m := &Module{Parallel: 3}
|
||||
failed, err := m.lintAll(context.Background(), run, tree, dirs, func(string) []string { return nil })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
slices.Sort(vetted)
|
||||
if strings.Join(vetted, " | ") != "vet -tags integration ./... | vet ./..." {
|
||||
t.Errorf("vet ran %v, want the pass without tags and the pass with them", vetted)
|
||||
if most < 2 || most > 3 {
|
||||
t.Errorf("%d lint commands ran at once, want 2 or 3 (the bound)", most)
|
||||
}
|
||||
if len(failed) != 2 || failed[0].Package != "b" || failed[1].Package != "e" {
|
||||
t.Errorf("failed = %+v, want b's then e's", failed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
)
|
||||
|
||||
func fetchOnce(t *testing.T, m *Module, states *keptStates, args map[string]any) FetchResult {
|
||||
@@ -189,61 +188,3 @@ func TestTheWorkspaceIsSelfContainedAfterAFetchThroughTheMirror(t *testing.T) {
|
||||
t.Errorf("the workspace is not whole: %v %s", err, out)
|
||||
}
|
||||
}
|
||||
|
||||
// 1921/S6: with a base, the mirror is brought to the trunk commit too, and the merge preview deepens the tree from the
|
||||
// mirror: it asks the forge for no history the mirror holds.
|
||||
func TestTheMirrorIsBroughtToTheBaseAndThePreviewFetchesFromIt(t *testing.T) {
|
||||
f := &fakeStep{gomods: "go.mod\n", rule: previewRules(false)}
|
||||
m, states := fetchModule(t, f)
|
||||
mirror := filepath.Join(m.Cache, "mirror", "novox", "mesh-sdk.git")
|
||||
f.files = map[string]string{"modules/m/module.json": "{}\n", "modules/m/go.sum": "sum\n", "go.mod": "module x\n", "go.sum": ""}
|
||||
fetchOnce(t, m, states, map[string]any{"repository": "novox/mesh-sdk", "commit": commitSHA, "base": baseSHA})
|
||||
lines := strings.Join(f.lines(), "\n")
|
||||
if !strings.Contains(lines, "-C "+mirror+" fetch --no-tags origin "+baseSHA) {
|
||||
t.Errorf("the mirror was not brought to the base:\n%s", lines)
|
||||
}
|
||||
if !strings.Contains(lines, "fetch --no-tags --unshallow "+mirror+" "+baseSHA) || strings.Contains(lines, "--unshallow origin") {
|
||||
t.Errorf("the preview fetched the trunk's history from the forge:\n%s", lines)
|
||||
}
|
||||
}
|
||||
|
||||
// 1921/S6: the siblings are placed side by side: each sibling's fetch has started before any of them ends.
|
||||
func TestTheSiblingsArePlacedAtOnce(t *testing.T) {
|
||||
var mu sync.Mutex
|
||||
started := map[string]bool{}
|
||||
both := make(chan struct{})
|
||||
f := &fakeStep{gomods: "go.mod\n"}
|
||||
f.rule = func(line string) (forgekey.Ran, bool) {
|
||||
if !strings.Contains(line, "/beside/") || !strings.Contains(line, " fetch --no-tags --depth 1 ") {
|
||||
return forgekey.Ran{}, false
|
||||
}
|
||||
mu.Lock()
|
||||
for _, name := range []string{"mesh-catalog", "mesh-host"} {
|
||||
if strings.Contains(line, "/beside/"+name) {
|
||||
started[name] = true
|
||||
}
|
||||
}
|
||||
if len(started) == 2 {
|
||||
select {
|
||||
case <-both:
|
||||
default:
|
||||
close(both)
|
||||
}
|
||||
}
|
||||
mu.Unlock()
|
||||
select {
|
||||
case <-both:
|
||||
case <-time.After(2 * time.Second):
|
||||
return forgekey.Ran{Status: 1, Stderr: "the other sibling's fetch never started"}, true
|
||||
}
|
||||
return forgekey.Ran{}, false
|
||||
}
|
||||
m, states := fetchModule(t, f)
|
||||
res := fetchOnce(t, m, states, map[string]any{"repository": "novox/mesh-controller", "commit": commitSHA, "beside": []any{
|
||||
map[string]any{"repository": "novox/mesh-host", "commit": commitSHA},
|
||||
map[string]any{"repository": "novox/mesh-catalog", "commit": commitSHA},
|
||||
}})
|
||||
if len(res.Beside) != 2 || res.Beside[0].Dir != "mesh-catalog" || res.Beside[1].Dir != "mesh-host" {
|
||||
t.Errorf("beside = %+v, want both, in name order", res.Beside)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
||||
)
|
||||
@@ -35,9 +34,8 @@ func (m *Module) remote(repository string) string {
|
||||
}
|
||||
|
||||
// merge makes the merge preview of the head onto base and writes the answer into res.
|
||||
// opts are the fetch's options, with the mirror when it holds base.
|
||||
func (m *Module) merge(ctx context.Context, res *FetchResult, tree, remote, base, commit string, opts []buildstep.Option) error {
|
||||
mr, err := buildstep.MergeInto(ctx, m.Run, tree, remote, base, commit, opts...)
|
||||
func (m *Module) merge(ctx context.Context, res *FetchResult, tree, remote, base, commit string) error {
|
||||
mr, err := buildstep.MergeInto(ctx, m.Run, tree, remote, base, commit, m.FetchOptions...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -91,22 +89,23 @@ func (m *Module) besideDir(workspace string) string {
|
||||
// placeSiblings fetches each sibling into beside/<name> of the workspace root, as a plain directory. A symlink anywhere
|
||||
// in a placed tree fails the step naming it: the mesh creates none, and a test must not be led out of its sibling.
|
||||
func (m *Module) placeSiblings(ctx context.Context, root string, siblings []siblingArg) ([]Sibling, error) {
|
||||
// The siblings are placed side by side (issue 1921, try speed S6); the first failure in the order given is the step's.
|
||||
placed := make([]Sibling, len(siblings))
|
||||
errs := make([]error, len(siblings))
|
||||
var wg sync.WaitGroup
|
||||
for i, s := range siblings {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
placed[i], errs[i] = m.placeSibling(ctx, root, s)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
var placed []Sibling
|
||||
for _, s := range siblings {
|
||||
name := s.repository[strings.Index(s.repository, "/")+1:]
|
||||
dir := filepath.Join(root, "beside", name)
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := buildstep.FetchCommit(ctx, m.Run, dir, m.remote(s.repository), s.commit, s.ref, m.fetchOptions(ctx, s.repository, s.commit, s.ref)...); err != nil {
|
||||
return nil, fmt.Errorf("beside %s: %w", s.repository, err)
|
||||
}
|
||||
if err := m.dissociate(ctx, dir); err != nil {
|
||||
return nil, fmt.Errorf("beside %s: %w", s.repository, err)
|
||||
}
|
||||
if link := firstSymlink(dir); link != "" {
|
||||
return nil, fmt.Errorf("beside %s holds a symlink, %s", s.repository, link)
|
||||
}
|
||||
placed = append(placed, Sibling{Repository: s.repository, Commit: s.commit, Dir: name})
|
||||
}
|
||||
sort.Slice(placed, func(i, j int) bool { return placed[i].Dir < placed[j].Dir })
|
||||
if len(placed) > 0 {
|
||||
@@ -122,25 +121,6 @@ func (m *Module) placeSiblings(ctx context.Context, root string, siblings []sibl
|
||||
return placed, nil
|
||||
}
|
||||
|
||||
// placeSibling fetches one sibling into beside/<name> of the workspace root.
|
||||
func (m *Module) placeSibling(ctx context.Context, root string, s siblingArg) (Sibling, error) {
|
||||
name := s.repository[strings.Index(s.repository, "/")+1:]
|
||||
dir := filepath.Join(root, "beside", name)
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
return Sibling{}, err
|
||||
}
|
||||
if err := buildstep.FetchCommit(ctx, m.Run, dir, m.remote(s.repository), s.commit, s.ref, m.fetchOptions(ctx, s.repository, s.commit, s.ref)...); err != nil {
|
||||
return Sibling{}, fmt.Errorf("beside %s: %w", s.repository, err)
|
||||
}
|
||||
if err := m.dissociate(ctx, dir); err != nil {
|
||||
return Sibling{}, fmt.Errorf("beside %s: %w", s.repository, err)
|
||||
}
|
||||
if link := firstSymlink(dir); link != "" {
|
||||
return Sibling{}, fmt.Errorf("beside %s holds a symlink, %s", s.repository, link)
|
||||
}
|
||||
return Sibling{Repository: s.repository, Commit: s.commit, Dir: name}, nil
|
||||
}
|
||||
|
||||
// besideRecord is the file in a workspace's root naming the siblings the fetch placed beside it.
|
||||
const besideRecord = "beside.json"
|
||||
|
||||
|
||||
@@ -85,8 +85,7 @@ func TestFetchWithABaseMergesAndAnswersThePreview(t *testing.T) {
|
||||
t.Errorf("trees = %+v", res.Trees)
|
||||
}
|
||||
lines := strings.Join(f.lines(), "\n")
|
||||
mirror := filepath.Join(m.Cache, "mirror", "novox", "mesh-sdk.git")
|
||||
for _, want := range []string{"fetch --no-tags --unshallow " + mirror + " " + baseSHA, "checkout -q --detach " + baseSHA,
|
||||
for _, want := range []string{"fetch --no-tags --unshallow origin " + baseSHA, "checkout -q --detach " + baseSHA,
|
||||
"core.hooksPath=/dev/null", "merge --no-ff --no-edit --no-verify"} {
|
||||
if !strings.Contains(lines, want) {
|
||||
t.Errorf("git was not asked %q:\n%s", want, lines)
|
||||
@@ -291,8 +290,8 @@ func TestTheIntegrationLevelIsGivenTheBesideDirectoryOnlyWhenSiblingsWerePlaced(
|
||||
}
|
||||
tested := 0
|
||||
for _, c := range f.calls {
|
||||
if !strings.HasPrefix(c.line, "go ") || !strings.Contains(c.line, "-tags integration") || strings.Contains(c.line, "-exec true") {
|
||||
continue // the setup's compile runs no test binary, so it is given nothing a test reads (issue 1921, S5)
|
||||
if !strings.HasPrefix(c.line, "go ") || !strings.Contains(c.line, "-tags integration") {
|
||||
continue
|
||||
}
|
||||
tested++
|
||||
got := ""
|
||||
|
||||
@@ -24,7 +24,9 @@ func threeModules(t *testing.T, f *integFake) (*Module, *keptStates) {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeTaggedTest(t, dir, "fixture/"+d)
|
||||
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module fixture/"+d+"\n\ngo 1.25.0\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return m, states
|
||||
}
|
||||
@@ -70,7 +72,7 @@ func TestOnlyTheSelectedDirectoryCompilesAndRuns(t *testing.T) {
|
||||
if !strings.HasPrefix(l, "go -C "+filepath.Join(tree, "b")+" test ") {
|
||||
t.Errorf("go test ran outside the selected directory: %s", l)
|
||||
}
|
||||
if strings.Contains(l, "-exec true") {
|
||||
if strings.Contains(l, "-run ^$") {
|
||||
compiled++
|
||||
} else {
|
||||
ran++
|
||||
@@ -89,7 +91,7 @@ func TestWithoutASelectionEveryDirectoryCompilesAndRuns(t *testing.T) {
|
||||
compiled, ran := 0, 0
|
||||
for _, l := range strings.Split(f.lines(), "\n") {
|
||||
if strings.HasPrefix(l, "go ") && strings.Contains(l, " test ") {
|
||||
if strings.Contains(l, "-exec true") {
|
||||
if strings.Contains(l, "-run ^$") {
|
||||
compiled++
|
||||
} else {
|
||||
ran++
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
package main
|
||||
|
||||
// The bound on the processes one ask starts (issue 1921, try speed S10; ADR 0541): the levels of an ask run side by side,
|
||||
// the unit level in this process and the integration level in a shut-off process of its own, and they share one bound,
|
||||
// the processors divided by the node's build capacity. The bound lives in the ask's workspace as lock files, one per
|
||||
// slot, so both processes count against it.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// slotsDir is the directory in a workspace's root holding its slots.
|
||||
const slotsDir = "slots"
|
||||
|
||||
// askDirs are the directories an ask keeps in its workspace's root beside its tree, gone with the workspace: its cache
|
||||
// layer and its bound on processes.
|
||||
var askDirs = []string{cacheLayerDir, slotsDir}
|
||||
|
||||
// numCPU is the node's processor count.
|
||||
func numCPU() int { return runtime.NumCPU() }
|
||||
|
||||
// slotPool is the bound of one workspace: n slots, each a lock file under dir.
|
||||
type slotPool struct {
|
||||
dir string
|
||||
n int
|
||||
next *atomic.Uint32
|
||||
}
|
||||
|
||||
// slotTurns spreads the waits of the commands that find every slot taken over the slots.
|
||||
var slotTurns atomic.Uint32
|
||||
|
||||
// slots is the bound of the workspace's processes, m.parallel() slots.
|
||||
func (m *Module) slots(workspace string) slotPool {
|
||||
return slotPool{dir: filepath.Join(m.Work, workspace, slotsDir), n: m.parallel(), next: &slotTurns}
|
||||
}
|
||||
|
||||
// acquire takes a slot of the pool, waiting for one when every slot is taken, and answers what frees it. A free slot is
|
||||
// taken at once; when none is, the wait is on one slot, taken in turn, until its holder frees it.
|
||||
func (p slotPool) acquire(ctx context.Context) (func(), error) {
|
||||
if err := os.MkdirAll(p.dir, 0o700); err != nil {
|
||||
return nil, fmt.Errorf("the workspace's bound on processes: %w", err)
|
||||
}
|
||||
take := func(i int, how int) (func(), bool, error) {
|
||||
f, err := os.OpenFile(filepath.Join(p.dir, "slot-"+strconv.Itoa(i)), os.O_CREATE|os.O_RDWR, 0o600)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if err := syscall.Flock(int(f.Fd()), how); err != nil {
|
||||
f.Close()
|
||||
if err == syscall.EWOULDBLOCK {
|
||||
return nil, false, nil
|
||||
}
|
||||
return nil, false, err
|
||||
}
|
||||
return func() { syscall.Flock(int(f.Fd()), syscall.LOCK_UN); f.Close() }, true, nil
|
||||
}
|
||||
for i := 0; i < p.n; i++ {
|
||||
release, ok, err := take(i, syscall.LOCK_EX|syscall.LOCK_NB)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the workspace's bound on processes: %w", err)
|
||||
}
|
||||
if ok {
|
||||
return release, nil
|
||||
}
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
release, _, err := take(int(p.next.Add(1))%p.n, syscall.LOCK_EX)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the workspace's bound on processes: %w", err)
|
||||
}
|
||||
return release, nil
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
)
|
||||
|
||||
// 1921/S10 (ADR 0541): the levels of one ask, running side by side, share one bound on the processes they start: the
|
||||
// bound is the workspace's, so a slot another level of the ask holds is one this level waits for.
|
||||
func TestTheLevelsOfAnAskShareOneBoundOnProcesses(t *testing.T) {
|
||||
m, states, ws, _ := workspaceFor(t, "failing")
|
||||
m.Parallel = 1
|
||||
held, err := m.slots(ws).acquire(context.Background()) // the other level holds the one slot
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var ran atomic.Int32
|
||||
inner := m.Run
|
||||
m.Run = func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
|
||||
if strings.Contains(strings.Join(args, " "), " go ") || name == "go" {
|
||||
ran.Add(1)
|
||||
}
|
||||
return inner(ctx, env, name, args...)
|
||||
}
|
||||
done := make(chan delivery.Step, 1)
|
||||
go func() { _, st := runUnit(t, m, states, ws); done <- st }()
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
if n := ran.Load(); n != 0 {
|
||||
t.Errorf("%d go commands ran while the ask's one slot was held by its other level", n)
|
||||
}
|
||||
held()
|
||||
select {
|
||||
case st := <-done:
|
||||
if st.State != delivery.StepPassed {
|
||||
t.Errorf("the step ended %s: %s", st.State, st.Reason)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("the level never ran after the slot was freed")
|
||||
}
|
||||
if ran.Load() == 0 {
|
||||
t.Error("no go command ran")
|
||||
}
|
||||
}
|
||||
|
||||
// 1921/S10 (ADR 0541): the bound is the processors divided by the node's build capacity, not each level half of them.
|
||||
func TestTheBoundIsTheProcessorsDividedByTheBuildCapacity(t *testing.T) {
|
||||
m := &Module{Capacity: func() int { return 3 }}
|
||||
if got, want := m.parallel(), max(1, numCPU()/3); got != want {
|
||||
t.Errorf("parallel = %d, want %d", got, want)
|
||||
}
|
||||
m = &Module{}
|
||||
if got, want := m.parallel(), max(1, numCPU()/2); got != want {
|
||||
t.Errorf("with no capacity known parallel = %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
package main
|
||||
|
||||
// The integration level runs the tests its tags add, not the unit tests again (issue 1921, try speed S7): `go test
|
||||
// -tags integration` without -run would run every untagged test too, and the unit level ran those already. Which tests a
|
||||
// tag adds is read from the source: a test function in a _test.go file whose //go:build line holds with the level's
|
||||
// tags and does not hold without them.
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/build/constraint"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// taggedRun is what the integration level runs in one go.mod directory: the packages holding a test the tags add, by
|
||||
// import path, and the -run pattern naming those tests.
|
||||
type taggedRun struct {
|
||||
packages []string
|
||||
pattern string
|
||||
}
|
||||
|
||||
// taggedTests reads, for each go.mod directory, the tests that tags add. selected are the packages the level was asked
|
||||
// to run (import paths, as build_go_digest keys them); none, or a relative pattern, is every package. A directory with no
|
||||
// test the tags add is left out of the answer.
|
||||
func taggedTests(tree string, dirs, selected, tags []string) (map[string]taggedRun, error) {
|
||||
want := map[string]bool{}
|
||||
for _, p := range selected {
|
||||
if strings.HasPrefix(p, "./") || p == "." {
|
||||
want = nil
|
||||
break
|
||||
}
|
||||
want[p] = true
|
||||
}
|
||||
if len(selected) == 0 {
|
||||
want = nil
|
||||
}
|
||||
with := map[string]bool{}
|
||||
for _, t := range tags {
|
||||
with[t] = true
|
||||
}
|
||||
out := map[string]taggedRun{}
|
||||
for _, d := range dirs {
|
||||
raw, _ := os.ReadFile(filepath.Join(tree, d, "go.mod"))
|
||||
mp := modulePath(string(raw))
|
||||
base := filepath.Join(tree, d)
|
||||
pkgs := map[string]bool{}
|
||||
names := map[string]bool{}
|
||||
err := lintable(base, func(path string) error {
|
||||
if !strings.HasSuffix(path, "_test.go") {
|
||||
return nil
|
||||
}
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expr := buildLine(src)
|
||||
if expr == nil || !expr.Eval(func(tag string) bool { return with[tag] || platformTag(tag) }) || expr.Eval(platformTag) {
|
||||
return nil // no constraint, or one the tags do not change: the unit level's
|
||||
}
|
||||
rel, _ := filepath.Rel(base, filepath.Dir(path))
|
||||
pkg := mp
|
||||
if rel != "." {
|
||||
pkg = mp + "/" + filepath.ToSlash(rel)
|
||||
}
|
||||
if want != nil && !want[pkg] {
|
||||
return nil
|
||||
}
|
||||
found := testNames(path, src)
|
||||
for _, n := range found {
|
||||
names[n] = true
|
||||
}
|
||||
if len(found) > 0 {
|
||||
pkgs[pkg] = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(pkgs) == 0 {
|
||||
continue
|
||||
}
|
||||
run := taggedRun{packages: sortedKeys(pkgs)}
|
||||
run.pattern = "^(" + strings.Join(sortedKeys(names), "|") + ")$"
|
||||
out[d] = run
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func sortedKeys(m map[string]bool) []string {
|
||||
out := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
out = append(out, k)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// buildLine is a file's //go:build constraint, nil when it has none.
|
||||
func buildLine(src []byte) constraint.Expr {
|
||||
for _, line := range strings.Split(string(src), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "package ") {
|
||||
break
|
||||
}
|
||||
if constraint.IsGoBuild(line) {
|
||||
if e, err := constraint.Parse(line); err == nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// platformTag is how a tag other than the level's evaluates, the same with the level's tags and without them: the
|
||||
// machine's platform, unix, cgo and every Go version hold; any other tag does not.
|
||||
func platformTag(tag string) bool {
|
||||
switch tag {
|
||||
case runtime.GOOS, runtime.GOARCH, "unix", "cgo", "gc":
|
||||
return true
|
||||
}
|
||||
return strings.HasPrefix(tag, "go1.")
|
||||
}
|
||||
|
||||
// testNames are the top-level test functions of a test file: func TestXxx, as go test runs them, TestMain left out.
|
||||
func testNames(path string, src []byte) []string {
|
||||
f, err := parser.ParseFile(token.NewFileSet(), path, src, parser.SkipObjectResolution)
|
||||
if err != nil {
|
||||
return nil // a file that does not parse fails the compile, which the run reports
|
||||
}
|
||||
var out []string
|
||||
for _, decl := range f.Decls {
|
||||
fn, ok := decl.(*ast.FuncDecl)
|
||||
if !ok || fn.Recv != nil || fn.Name.Name == "TestMain" || !isTestName(fn.Name.Name) {
|
||||
continue
|
||||
}
|
||||
out = append(out, fn.Name.Name)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// isTestName says whether go test runs a function of this name as a test: Test, then nothing or a rune that is not
|
||||
// lower case.
|
||||
func isTestName(name string) bool {
|
||||
rest, ok := strings.CutPrefix(name, "Test")
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if rest == "" {
|
||||
return true
|
||||
}
|
||||
r, _ := utf8.DecodeRuneInString(rest)
|
||||
return !unicode.IsLower(r)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -170,9 +171,6 @@ type levelOpts struct {
|
||||
// parallelism, where the unit level's is the slowest package's.
|
||||
setup time.Duration
|
||||
wall bool
|
||||
// runByDir is the -run pattern of each go.mod directory, over run: the tests the integration level's tags add there
|
||||
// (issue 1921, try speed S7).
|
||||
runByDir map[string]string
|
||||
// compose is a state of the level to put in the result (the store and the bus it ran against).
|
||||
store, bus string
|
||||
}
|
||||
@@ -207,56 +205,35 @@ func (m *Module) runLevel(ctx context.Context, tree string, o levelOpts) (*TestR
|
||||
todo = append(todo, d)
|
||||
}
|
||||
}
|
||||
// The level's commands share one bound on processes with each other and with the ask's other level: the lint's and
|
||||
// the tests' (issue 1921, try speed S4, S10).
|
||||
slots := m.slots(o.workspace)
|
||||
var lintFailed []Failure
|
||||
var lintErr error
|
||||
var lintDone chan struct{}
|
||||
var crash any // a panic of a run or of the lint goes on in the caller, which cleans up after it
|
||||
var crashMu sync.Mutex
|
||||
if o.level == LevelUnit {
|
||||
// gofmt and go vet of each directory the level runs, beside its tests (ADR 0529 §1; issue 1887 D7-go; issue 1921,
|
||||
// try speed S4): a directory that fails them fails the level, and their time is the result's Lint.
|
||||
lintDone = make(chan struct{})
|
||||
go func() {
|
||||
defer close(lintDone)
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
crashMu.Lock()
|
||||
crash = p
|
||||
crashMu.Unlock()
|
||||
}
|
||||
}()
|
||||
linted := m.clock()
|
||||
// go vet compiles and runs nothing of the change, so it warms the shared cache for every later check, a pull
|
||||
// request's head included (issue 1921, try speed S11; the operator's decision); the tests run beneath the
|
||||
// ask's own layer.
|
||||
compile, err := m.stepRunner(o.workspace, true)
|
||||
if err != nil {
|
||||
lintErr = err
|
||||
return
|
||||
}
|
||||
lintFailed, lintErr = m.lintAll(ctx, compile, tree, todo, func(d string) []string {
|
||||
return append(m.goEnvFor(filepath.Join(tree, d)), o.env...)
|
||||
}, byDir, slots.acquire)
|
||||
lintFailed = append(m.addedBinaries(ctx, m.Run, tree), lintFailed...) // git alone, no code of the change
|
||||
res.Lint = m.clock().Sub(linted).Seconds()
|
||||
}()
|
||||
// gofmt and go vet of each directory the level runs come before its tests (ADR 0529 §1; issue 1887 D7-go): a
|
||||
// directory that fails them fails the level at once, and their time is the result's Lint.
|
||||
linted := m.clock()
|
||||
failed, err := m.lintAll(ctx, run, tree, todo, func(d string) []string {
|
||||
return append(m.goEnvFor(filepath.Join(tree, d)), o.env...)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.Lint = m.clock().Sub(linted).Seconds()
|
||||
if len(failed) > 0 {
|
||||
res.Failed = failed
|
||||
res.judge(o.bounds)
|
||||
return res, nil
|
||||
}
|
||||
began = m.clock() // the level's running time is its tests'
|
||||
}
|
||||
runs := make([]dirRun, len(todo))
|
||||
slots := make(chan struct{}, m.parallel())
|
||||
var wg sync.WaitGroup
|
||||
var slotErr error
|
||||
var crash any // a panic of a run goes on in the caller, which cleans up after it
|
||||
var crashMu sync.Mutex
|
||||
for i, d := range todo {
|
||||
release, err := slots.acquire(ctx)
|
||||
if err != nil {
|
||||
slotErr = err
|
||||
break
|
||||
}
|
||||
wg.Add(1)
|
||||
slots <- struct{}{}
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer release()
|
||||
defer func() { <-slots }()
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
crashMu.Lock()
|
||||
@@ -268,9 +245,7 @@ func (m *Module) runLevel(ctx context.Context, tree string, o levelOpts) (*TestR
|
||||
if len(o.tags) > 0 {
|
||||
args = append(args, "-tags", strings.Join(o.tags, ","))
|
||||
}
|
||||
if r := o.runByDir[d]; r != "" {
|
||||
args = append(args, "-run", r)
|
||||
} else if o.run != "" {
|
||||
if o.run != "" {
|
||||
args = append(args, "-run", o.run)
|
||||
}
|
||||
args = append(args, byDir[d]...)
|
||||
@@ -283,19 +258,9 @@ func (m *Module) runLevel(ctx context.Context, tree string, o levelOpts) (*TestR
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
if lintDone != nil {
|
||||
<-lintDone
|
||||
}
|
||||
if crash != nil {
|
||||
panic(crash)
|
||||
}
|
||||
if lintErr != nil {
|
||||
return nil, lintErr
|
||||
}
|
||||
if slotErr != nil {
|
||||
return nil, slotErr
|
||||
}
|
||||
res.Failed = append(res.Failed, lintFailed...) // the lint's failures first, then the tests'
|
||||
for i, d := range todo {
|
||||
dr := runs[i]
|
||||
if (dr.r.Status != 0 || dr.r.Err != "") && len(dr.rd.pkgs) == 0 {
|
||||
@@ -321,20 +286,12 @@ func (m *Module) runLevel(ctx context.Context, tree string, o levelOpts) (*TestR
|
||||
// runAnswer is what a step runner answers.
|
||||
type runAnswer = forgekey.Ran
|
||||
|
||||
// parallel is how many processes the levels of one ask start at once, together (ADR 0541; issue 1921, try speed S10):
|
||||
// the processors divided by the node's build capacity, half of them when the capacity is not known.
|
||||
// parallel is how many directories a level runs at once.
|
||||
func (m *Module) parallel() int {
|
||||
if m.Parallel > 0 {
|
||||
return m.Parallel
|
||||
}
|
||||
capacity := 0
|
||||
if m.Capacity != nil {
|
||||
capacity = m.Capacity()
|
||||
}
|
||||
if capacity <= 0 {
|
||||
capacity = 2
|
||||
}
|
||||
return max(1, numCPU()/capacity)
|
||||
return max(1, runtime.NumCPU()/2)
|
||||
}
|
||||
|
||||
// packagesByDir says which patterns run in which go.mod directory. A relative pattern (./...) runs in every directory, as
|
||||
|
||||
@@ -66,9 +66,7 @@ func TestUnitLevelRunsTheRealGoOnTheFixtureRepositories(t *testing.T) {
|
||||
"tagged": {passed: true, noTests: true},
|
||||
"empty": {passed: true, noTests: true},
|
||||
"slow": {passed: true},
|
||||
// go vet, the lint step beside the tests (ADR 0542 §2), names the directory and the compiler's line; the test run
|
||||
// of the package that does not compile fails beside it
|
||||
"broken": {failed: "."},
|
||||
"broken": {failed: "."}, // go vet, before the tests since R4g (ADR 0529 §1), names the directory and the compiler's line
|
||||
}
|
||||
for name, w := range want {
|
||||
home := t.TempDir()
|
||||
@@ -86,16 +84,14 @@ func TestUnitLevelRunsTheRealGoOnTheFixtureRepositories(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
lintFirst := w.failed != "." || (len(res.Failed) >= 1 && res.Failed[0].Step == StepLint)
|
||||
if res.Passed != w.passed || res.NoTests != w.noTests || !lintFirst ||
|
||||
(w.failed != "" && (len(res.Failed) == 0 || res.Failed[0].Package != w.failed)) {
|
||||
if res.Passed != w.passed || res.NoTests != w.noTests || (w.failed != "" && (len(res.Failed) != 1 || res.Failed[0].Package != w.failed)) {
|
||||
t.Errorf("%s: result = %+v", name, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1887/D7-go against the real go: a tree holding an unformatted file fails the check naming the file, as the lint step's
|
||||
// failure (ADR 0542 §2), and the level's result carries the lint's time.
|
||||
// 1887/D7-go against the real go: a tree holding an unformatted file fails the unit level naming the file, before any
|
||||
// test runs, and the level's result carries the lint's time.
|
||||
func TestAnUnformattedFileFailsTheRealUnitLevel(t *testing.T) {
|
||||
needTools(t, "the unit level's gofmt and vet", "go", "git", "bwrap")
|
||||
home := t.TempDir()
|
||||
@@ -115,9 +111,8 @@ func TestAnUnformattedFileFailsTheRealUnitLevel(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.Passed || len(res.Failed) == 0 || res.Failed[0].Test != "gofmt" || res.Failed[0].Step != StepLint ||
|
||||
!strings.Contains(res.Failed[0].Output, "ugly.go") || res.Lint <= 0 {
|
||||
t.Fatalf("result = %+v, want the lint step failed by gofmt naming ugly.go, with its time", res)
|
||||
if res.Passed || len(res.Failed) == 0 || res.Failed[0].Test != "gofmt" || !strings.Contains(res.Failed[0].Output, "ugly.go") || res.Tests != 0 {
|
||||
t.Fatalf("result = %+v, want the level failed by gofmt naming ugly.go and no test run", res)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,8 +156,9 @@ func TestIntegrationLevelRunsAgainstARealStoreAndBusAndLeavesNothing(t *testing.
|
||||
if !res.Passed || res.Tests != 1 || res.Setup <= 0 || res.Setup > IntegrationSetup.Seconds() {
|
||||
t.Errorf("result = %+v", res)
|
||||
}
|
||||
if left := workspaceLeft(filepath.Join(work, "ws1")); len(left) != 1 || left[0] != "tree" {
|
||||
t.Errorf("the workspace holds %v besides the ask's own directories, want only its tree: the run directory is gone", left)
|
||||
entries, _ := os.ReadDir(filepath.Join(work, "ws1"))
|
||||
if len(entries) != 1 || entries[0].Name() != "tree" {
|
||||
t.Errorf("the workspace holds %v, want only its tree: the run directory is gone", entries)
|
||||
}
|
||||
for deadline := time.Now().Add(5 * time.Second); len(processesNamed(work)) > 0 && time.Now().Before(deadline); time.Sleep(100 * time.Millisecond) {
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -26,11 +25,8 @@ func capturedRunner(t *testing.T, captured string, calls *[]cmdRecord) forgekey.
|
||||
if strings.Contains(string(raw), `"Action":"fail"`) {
|
||||
status = 1 // go test exits 1 when a test or a build failed
|
||||
}
|
||||
var mu sync.Mutex // the lint and the tests of a level run at once (issue 1921, try speed S4)
|
||||
return func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
|
||||
mu.Lock()
|
||||
*calls = append(*calls, cmdRecord{env: env, line: name + " " + strings.Join(args, " ")})
|
||||
mu.Unlock()
|
||||
switch {
|
||||
case name == "git" && args[2] == "ls-files":
|
||||
return forgekey.Ran{Stdout: "go.mod\n"}
|
||||
@@ -107,33 +103,6 @@ func TestUnitLevelNamesTheFailingTestAndFailsTheLevel(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 1921: a fixture with a failing test in each of two packages: one level's run names both, each with its own
|
||||
// cause, and counts every test, the passing one beside a failure too. go test runs every package to its end; nothing
|
||||
// stops at the first failure.
|
||||
func TestUnitLevelNamesEveryFailingTestOfEveryPackage(t *testing.T) {
|
||||
m, states, ws, calls := workspaceFor(t, "twofailing")
|
||||
res, st := runUnit(t, m, states, ws)
|
||||
if st.State != delivery.StepPassed || res.Passed {
|
||||
t.Fatalf("step %s (%s), result %+v: want the step done and the level failed", st.State, st.Reason, res)
|
||||
}
|
||||
var got []string
|
||||
for _, f := range res.Failed {
|
||||
got = append(got, f.Package+"."+f.Test+": "+f.Cause)
|
||||
}
|
||||
want := []string{"fixture/twofailing/one.TestOneIsTwo: One() = 1, want 2", "fixture/twofailing/two.TestTwoIsThree: Two() = 2, want 3"}
|
||||
if strings.Join(got, "\n") != strings.Join(want, "\n") {
|
||||
t.Errorf("failed\n%s\nwant\n%s", strings.Join(got, "\n"), strings.Join(want, "\n"))
|
||||
}
|
||||
if res.Tests != 3 {
|
||||
t.Errorf("%d tests counted, want 3", res.Tests)
|
||||
}
|
||||
for _, c := range *calls {
|
||||
if strings.Contains(c.line, " test ") && strings.Contains(c.line, "-failfast") {
|
||||
t.Errorf("go test ran with -failfast: %s", c.line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1290/AC2: a fixture whose tests take more than the bound (lowered by the module's option, not by waiting five
|
||||
// seconds): the level fails naming the bound and the slowest tests; under the real bound the same run passes.
|
||||
func TestUnitLevelOverItsBoundFailsNamingTheBoundAndTheSlowestTests(t *testing.T) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
Each file is the standard output of `GOTOOLCHAIN=go1.27.1 GOFLAGS=-mod=mod go test -count=1 -json ./...` (go1.27.1)
|
||||
run in the fixture module of the same name under ../repos, captured on 2026-10-20 for issue 1290. The unit
|
||||
tests feed them to the parser through a fake runner, so no test here runs `go test`. twofailing.json was captured the
|
||||
same way on 2026-10-24 for issue 1921: two packages with a failing test each, and a passing test beside one of them.
|
||||
tests feed them to the parser through a fake runner, so no test here runs `go test`.
|
||||
|
||||
golist-external.json is `GOTOOLCHAIN=go1.27.1 GOFLAGS=-mod=mod go list -json=ImportPath,Standard,DepOnly,Module
|
||||
git.novox.be/novox/mesh-sdk/go/buildstep` run in this module (go1.27.1, 2026-10-20 for issue 1291): a package of a module
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{"Time":"2026-10-24T23:45:37.721718705+02:00","Action":"start","Package":"fixture/twofailing/one"}
|
||||
{"Time":"2026-10-24T23:45:37.723685152+02:00","Action":"run","Package":"fixture/twofailing/one","Test":"TestOneIsTwo"}
|
||||
{"Time":"2026-10-24T23:45:37.723726472+02:00","Action":"output","Package":"fixture/twofailing/one","Test":"TestOneIsTwo","Output":"=== RUN TestOneIsTwo\n","OutputType":"frame"}
|
||||
{"Time":"2026-10-24T23:45:37.723743682+02:00","Action":"output","Package":"fixture/twofailing/one","Test":"TestOneIsTwo","Output":" one_test.go:6: checking One\n"}
|
||||
{"Time":"2026-10-24T23:45:37.723750622+02:00","Action":"output","Package":"fixture/twofailing/one","Test":"TestOneIsTwo","Output":" one_test.go:8: One() = 1, want 2\n","OutputType":"error"}
|
||||
{"Time":"2026-10-24T23:45:37.723759302+02:00","Action":"output","Package":"fixture/twofailing/one","Test":"TestOneIsTwo","Output":"--- FAIL: TestOneIsTwo (0.00s)\n","OutputType":"frame"}
|
||||
{"Time":"2026-10-24T23:45:37.723889303+02:00","Action":"start","Package":"fixture/twofailing/two"}
|
||||
{"Time":"2026-10-24T23:45:37.723762972+02:00","Action":"fail","Package":"fixture/twofailing/one","Test":"TestOneIsTwo","Elapsed":0}
|
||||
{"Time":"2026-10-24T23:45:37.723952274+02:00","Action":"run","Package":"fixture/twofailing/one","Test":"TestOneIsOne"}
|
||||
{"Time":"2026-10-24T23:45:37.723955484+02:00","Action":"output","Package":"fixture/twofailing/one","Test":"TestOneIsOne","Output":"=== RUN TestOneIsOne\n","OutputType":"frame"}
|
||||
{"Time":"2026-10-24T23:45:37.723959304+02:00","Action":"output","Package":"fixture/twofailing/one","Test":"TestOneIsOne","Output":"--- PASS: TestOneIsOne (0.00s)\n","OutputType":"frame"}
|
||||
{"Time":"2026-10-24T23:45:37.723964914+02:00","Action":"pass","Package":"fixture/twofailing/one","Test":"TestOneIsOne","Elapsed":0}
|
||||
{"Time":"2026-10-24T23:45:37.723967704+02:00","Action":"output","Package":"fixture/twofailing/one","Output":"FAIL\n","OutputType":"frame"}
|
||||
{"Time":"2026-10-24T23:45:37.724225126+02:00","Action":"output","Package":"fixture/twofailing/one","Output":"FAIL\tfixture/twofailing/one\t0.002s\n","OutputType":"frame"}
|
||||
{"Time":"2026-10-24T23:45:37.724231936+02:00","Action":"fail","Package":"fixture/twofailing/one","Elapsed":0.003}
|
||||
{"Time":"2026-10-24T23:45:37.725752949+02:00","Action":"run","Package":"fixture/twofailing/two","Test":"TestTwoIsThree"}
|
||||
{"Time":"2026-10-24T23:45:37.725796309+02:00","Action":"output","Package":"fixture/twofailing/two","Test":"TestTwoIsThree","Output":"=== RUN TestTwoIsThree\n","OutputType":"frame"}
|
||||
{"Time":"2026-10-24T23:45:37.725808239+02:00","Action":"output","Package":"fixture/twofailing/two","Test":"TestTwoIsThree","Output":" two_test.go:7: Two() = 2, want 3\n","OutputType":"error"}
|
||||
{"Time":"2026-10-24T23:45:37.72581671+02:00","Action":"output","Package":"fixture/twofailing/two","Test":"TestTwoIsThree","Output":"--- FAIL: TestTwoIsThree (0.00s)\n","OutputType":"frame"}
|
||||
{"Time":"2026-10-24T23:45:37.72582026+02:00","Action":"fail","Package":"fixture/twofailing/two","Test":"TestTwoIsThree","Elapsed":0}
|
||||
{"Time":"2026-10-24T23:45:37.72582549+02:00","Action":"output","Package":"fixture/twofailing/two","Output":"FAIL\n","OutputType":"frame"}
|
||||
{"Time":"2026-10-24T23:45:37.726319344+02:00","Action":"output","Package":"fixture/twofailing/two","Output":"FAIL\tfixture/twofailing/two\t0.002s\n","OutputType":"frame"}
|
||||
{"Time":"2026-10-24T23:45:37.726362544+02:00","Action":"fail","Package":"fixture/twofailing/two","Elapsed":0.002}
|
||||
@@ -1,3 +0,0 @@
|
||||
module fixture/twofailing
|
||||
|
||||
go 1.25.0
|
||||
@@ -1,3 +0,0 @@
|
||||
package one
|
||||
|
||||
func One() int { return 1 }
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
package one
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestOneIsTwo(t *testing.T) {
|
||||
t.Log("checking One")
|
||||
if One() != 2 {
|
||||
t.Fatalf("One() = %d, want 2", One())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOneIsOne(t *testing.T) {
|
||||
if One() != 1 {
|
||||
t.Fatal("one")
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package two
|
||||
|
||||
func Two() int { return 2 }
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
package two
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestTwoIsThree(t *testing.T) {
|
||||
if Two() != 3 {
|
||||
t.Errorf("Two() = %d, want 3", Two())
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ module meshbuildgo
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024232922-99407c37df95
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024125710-bfc3883af980
|
||||
github.com/distribution/distribution/v3 v3.1.2
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024214154-bc16740feb0d h1:oJREw0ggKqJWJ5DEISSL0i2ZC5XqdXAidxo9NJ74TCc=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024214154-bc16740feb0d/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024225846-cb548a6a588c h1:GWHP9ozfkFXHVEL2gE4Xa5kc5e8uPO+UL9+mau6iaIs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024225846-cb548a6a588c/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024230839-4a12ccb78d19 h1:NqU4hkk/jHdTEcGFnnXvdOJwD8dH5u+qWaCdQQ+JYMc=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024230839-4a12ccb78d19/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024232250-9b408db24a58 h1:4Damtdqlf6u6nJhvMA99peDy1jmjixtcwFXJ4bWEkLk=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024232250-9b408db24a58/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024232922-99407c37df95 h1:I8zbgGzRjmgYydPTWo8hmaZW/P/g+qGDUfu3E32/pkU=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024232922-99407c37df95/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024125710-bfc3883af980 h1:ZEEeYibEPD1rngOAqbODzRY5rUStGC46OpyQwtNCYbs=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024125710-bfc3883af980/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
||||
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
|
||||
@@ -3,7 +3,7 @@ module meshbuildnode
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024232922-99407c37df95
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023213200-403a5d72ab93
|
||||
github.com/distribution/distribution/v3 v3.1.2
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024214154-bc16740feb0d h1:oJREw0ggKqJWJ5DEISSL0i2ZC5XqdXAidxo9NJ74TCc=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024214154-bc16740feb0d/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024232922-99407c37df95 h1:I8zbgGzRjmgYydPTWo8hmaZW/P/g+qGDUfu3E32/pkU=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024232922-99407c37df95/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023213200-403a5d72ab93 h1:FAiFY2KBKEoZSH49OZTt7/zRBIQxyl5g/KhYrpggbN4=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023213200-403a5d72ab93/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
||||
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -153,6 +155,9 @@ type state struct {
|
||||
// unplaced is a module whose last fill failed, for want of the node-engine's write socket (issue 1165) or by the
|
||||
// engine's own refusal (issue 1873): filled again when the socket is made again.
|
||||
unplaced map[string]bool
|
||||
// ownComposed is the digest of the node's own composition the node-engine last said it runs (its have
|
||||
// `<node>.node`): when it moves, every module is filled again (issue 1945).
|
||||
ownComposed string
|
||||
// bound is each provider's bucket of entries this client watches, by key: this machine's consumers' entries alone;
|
||||
// boundReady is a bucket whose current entries are all read, and boundUsers the modules filled from each.
|
||||
bound map[string]map[string][]byte
|
||||
@@ -225,6 +230,21 @@ func (c *Client) Run(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Its node's own have, one key: the node-engine moving to a new composition of the node's own fills every module
|
||||
// again, since an apply may have left a filled file as the composition had it (issue 1945, live on 2026-10-25). A
|
||||
// bucket not there yet (a node-engine before release 6) is said, and nothing waits for it.
|
||||
if haveKV, err := c.JS.KeyValue(ctx, delivery.HaveBucket); err == nil {
|
||||
ownKey, err := delivery.HaveNodeKey(c.Node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
kvs[delivery.HaveBucket] = haveKV
|
||||
if err := watch(delivery.HaveBucket, ownKey); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "the node's own have is not watched, so a new composition of the node's own fills nothing again: %v\n", err)
|
||||
}
|
||||
// A provider's bucket of entries, watched once a plan here names that provider (consumers.go): this machine's
|
||||
// consumers' keys alone. A bucket not there yet is tried again at the next fill.
|
||||
watchedBound := map[string]bool{}
|
||||
@@ -310,6 +330,13 @@ func (c *Client) Run(ctx context.Context) error {
|
||||
if u.socketUp {
|
||||
continue // the first fill below tries every file
|
||||
}
|
||||
if u.bucket == delivery.HaveBucket {
|
||||
// The composition the node runs now: the first fill below fills every module anyway.
|
||||
if u.entry != nil {
|
||||
s.ownComposed = ownComposedOf(u.entry)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if u.changed != nil {
|
||||
// Nothing is held yet: every secret is asked for when it is first filled.
|
||||
continue
|
||||
@@ -355,9 +382,27 @@ func (c *Client) Run(ctx context.Context) error {
|
||||
case u.asked != nil:
|
||||
c.respond(s, u.asked)
|
||||
case u.socketUp:
|
||||
// Every module, not only those whose fill failed: the node-engine that made the socket may have
|
||||
// applied a composition that left a filled file as the composition had it (issue 1945).
|
||||
for m := range s.unplaced {
|
||||
dirty[m] = true
|
||||
}
|
||||
for m := range s.plans {
|
||||
dirty[m] = true
|
||||
}
|
||||
case u.bucket == delivery.HaveBucket:
|
||||
if u.entry == nil {
|
||||
break
|
||||
}
|
||||
now := ownComposedOf(u.entry)
|
||||
if now != "" && s.ownComposed != "" && now != s.ownComposed {
|
||||
for m := range s.plans {
|
||||
dirty[m] = true
|
||||
}
|
||||
}
|
||||
if now != "" {
|
||||
s.ownComposed = now
|
||||
}
|
||||
case u.changed != nil:
|
||||
for _, m := range c.secretChanged(s, u.changed) {
|
||||
dirty[m] = true
|
||||
@@ -397,6 +442,19 @@ func (c *Client) Run(ctx context.Context) error {
|
||||
|
||||
func hasPlan(s *state, m string) bool { _, ok := s.plans[m]; return ok }
|
||||
|
||||
// ownComposedOf is the digest of the node's own composition a have `<node>.node` says the node-engine runs; empty for a
|
||||
// deleted key or a value it cannot read.
|
||||
func ownComposedOf(e jetstream.KeyValueEntry) string {
|
||||
if e.Operation() != jetstream.KeyValuePut {
|
||||
return ""
|
||||
}
|
||||
h, err := delivery.DecodeHaveNode(e.Value())
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return h.Composed
|
||||
}
|
||||
|
||||
// told says what an update moved of a module's config, or of its placement, which decides whether it is answered
|
||||
// at all (asks.go).
|
||||
func (c *Client) told(u update) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -81,7 +82,7 @@ func startBus(t *testing.T) *aBus {
|
||||
}
|
||||
b := &aBus{url: s.ClientURL(), domain: js, conn: nc, kv: map[string]jetstream.KeyValue{}}
|
||||
for _, name := range []string{AssignmentsBucket, ConfigBucket, RegistryBucket, NeedsBucket, AppliedBucket,
|
||||
CollectBucket} {
|
||||
CollectBucket, delivery.HaveBucket} {
|
||||
kv, err := js.CreateKeyValue(context.Background(), jetstream.KeyValueConfig{Bucket: name})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -741,3 +742,39 @@ func (b *aBus) defaults(t *testing.T, module string, manifest []byte) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The node-engine moving to a new composition of the node's own fills every module again: an apply may have left a
|
||||
// filled file as the composition had it (issue 1945, live on 2026-10-25: release 6's node-engines wrote filled files
|
||||
// empty, and the client, which fills at its start and on a change of a value, filled nothing again).
|
||||
func TestNatsTheNodesOwnHaveMovingFillsEveryModuleAgain(t *testing.T) {
|
||||
b := startBus(t)
|
||||
e := startEngine(t, map[string]string{dunstrc: "owned"})
|
||||
own := func(digit string) {
|
||||
raw, err := delivery.EncodeHaveNode(delivery.HaveNode{Composed: "sha256:" + strings.Repeat(digit, 64)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b.put(t, delivery.HaveBucket, node+".node", string(raw))
|
||||
}
|
||||
own("a")
|
||||
b.config(t, "dunst", "font-size", "13")
|
||||
b.assign(t, "dunst", dunst(t))
|
||||
aClient(t, b, e, t.TempDir())
|
||||
eventually(t, "dunstrc is filled", func() bool { return strings.Contains(e.read(t, dunstrc), "Monospace 13") })
|
||||
|
||||
// The node's own composition empties it, and the node-engine says it runs that composition.
|
||||
if err := os.WriteFile(e.at(dunstrc), nil, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
own("b")
|
||||
eventually(t, "dunstrc is filled again", func() bool { return strings.Contains(e.read(t, dunstrc), "Monospace 13") })
|
||||
|
||||
// The same composition said again fills nothing.
|
||||
_, before := e.counts(dunstrc)
|
||||
own("b")
|
||||
b.config(t, "dunst", "font-size", "15")
|
||||
eventually(t, "the change is written", func() bool { return strings.Contains(e.read(t, dunstrc), "Monospace 15") })
|
||||
if _, after := e.counts(dunstrc); after-before != 1 {
|
||||
t.Errorf("dunstrc was asked %d times for one change", after-before)
|
||||
}
|
||||
}
|
||||
|
||||
+9
-9
@@ -41,7 +41,10 @@ func TestNatsAFillThatFailedForWantOfTheSocketIsTriedWhenTheSocketReturns(t *tes
|
||||
})
|
||||
}
|
||||
|
||||
func TestNatsAReturningSocketAsksNothingOfAModuleThatWasPlaced(t *testing.T) {
|
||||
// A returning socket fills every module again, a placed one too: the node-engine that made it may have applied a
|
||||
// composition that left a filled file as the composition had it (issue 1945, live on 2026-10-25: release 6's node-engines
|
||||
// wrote claude-code's, node-env's and power's filled files empty, and nothing filled them again).
|
||||
func TestNatsAReturningSocketFillsAPlacedModuleAgain(t *testing.T) {
|
||||
b := startBus(t)
|
||||
e := newEngine(t, map[string]string{dunstrc: "owned"})
|
||||
stop := e.listen(t)
|
||||
@@ -49,17 +52,14 @@ func TestNatsAReturningSocketAsksNothingOfAModuleThatWasPlaced(t *testing.T) {
|
||||
b.assign(t, "dunst", dunst(t))
|
||||
aClient(t, b, e, t.TempDir())
|
||||
eventually(t, "dunstrc is filled", func() bool { return strings.Contains(e.read(t, dunstrc), "Monospace 13") })
|
||||
_, before := e.counts(dunstrc)
|
||||
|
||||
// The node-engine replaces itself: the socket goes and returns, and a later change proves the loop ran past it.
|
||||
// The node-engine replaces itself and, before it returns, the file is emptied.
|
||||
stop()
|
||||
e.listen(t)
|
||||
b.config(t, "dunst", "font-size", "15")
|
||||
eventually(t, "the change is written", func() bool { return strings.Contains(e.read(t, dunstrc), "Monospace 15") })
|
||||
_, after := e.counts(dunstrc)
|
||||
if after-before != 1 {
|
||||
t.Errorf("dunstrc was asked %d times for one change", after-before)
|
||||
if err := os.WriteFile(e.at(dunstrc), nil, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
e.listen(t)
|
||||
eventually(t, "dunstrc is filled again", func() bool { return strings.Contains(e.read(t, dunstrc), "Monospace 13") })
|
||||
}
|
||||
|
||||
func TestWatchSocketWakesWhenTheSocketIsMadeAndWhenItsDirectoryReturns(t *testing.T) {
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
# The provisioner client's account as mesh-controller composes it (internal/broker ComposeAccounts, feature branch
|
||||
# 57-config-and-provisioning with issue 647, which grants it the vault's secret-changed, issue 685, which lets its
|
||||
# own machine's tool runner ask it, and issue 679, which lets it read the whole assignments mirror and its machine's
|
||||
# keys of a provider's bucket of entries, here postgres_bound, and for gitea's route, route-adapter_bound and route-proxy_bound, issue 792; node-tools_bound for claude-code's console port, issue 761), the tool runner's account of the same machine
|
||||
# keys of a provider's bucket of entries, here postgres_bound, and for gitea's route, route-adapter_bound and route-proxy_bound, issue 792; node-tools_bound for claude-code's console port, issue 761; its node's own have, node-engine_have's g14.node, issue 1945), the tool runner's account of the same machine
|
||||
# carrying claude-code, and a `domain` user granted everything, standing for the controller, mesh-config and the
|
||||
# vault, which own the buckets the client reads and answer its asks. Composed again if the client's grants change.
|
||||
|
||||
@@ -11,7 +11,7 @@ accounts {
|
||||
users = [
|
||||
{ user: "domain", password: "x" }
|
||||
{ user: "provisioner.g14", password: "x", permissions: {
|
||||
publish: { allow: ["$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.g14.>", "$JS.API.CONSUMER.CREATE.KV_mesh-controller_assignments.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_applied.*.$KV.mesh-provisioner_applied.g14.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_collect.*.$KV.mesh-provisioner_collect.g14.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_needs.*.$KV.mesh-provisioner_needs.g14.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_registry.>", "$JS.API.CONSUMER.CREATE.KV_postgres_bound.*.$KV.postgres_bound.g14.>", "$JS.API.CONSUMER.CREATE.KV_route-adapter_bound.*.$KV.route-adapter_bound.g14.>", "$JS.API.CONSUMER.CREATE.KV_route-proxy_bound.*.$KV.route-proxy_bound.g14.>", "$JS.API.CONSUMER.CREATE.KV_node-tools_bound.*.$KV.node-tools_bound.g14.>", "$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.>", "$JS.API.CONSUMER.DELETE.KV_mesh-controller_assignments.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_applied.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_collect.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_needs.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_registry.>", "$JS.API.CONSUMER.DELETE.KV_postgres_bound.>", "$JS.API.CONSUMER.DELETE.KV_route-adapter_bound.>", "$JS.API.CONSUMER.DELETE.KV_route-proxy_bound.>", "$JS.API.CONSUMER.DELETE.KV_node-tools_bound.>", "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.g14.>", "$JS.API.DIRECT.GET.KV_mesh-controller_assignments.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_applied.$KV.mesh-provisioner_applied.g14.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_collect.$KV.mesh-provisioner_collect.g14.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_needs.$KV.mesh-provisioner_needs.g14.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_registry.>", "$JS.API.DIRECT.GET.KV_postgres_bound.$KV.postgres_bound.g14.>", "$JS.API.DIRECT.GET.KV_route-adapter_bound.$KV.route-adapter_bound.g14.>", "$JS.API.DIRECT.GET.KV_route-proxy_bound.$KV.route-proxy_bound.g14.>", "$JS.API.DIRECT.GET.KV_node-tools_bound.$KV.node-tools_bound.g14.>", "$JS.API.STREAM.INFO.KV_mesh-config_resolved", "$JS.API.STREAM.INFO.KV_mesh-controller_assignments", "$JS.API.STREAM.INFO.KV_mesh-provisioner_applied", "$JS.API.STREAM.INFO.KV_mesh-provisioner_collect", "$JS.API.STREAM.INFO.KV_mesh-provisioner_needs", "$JS.API.STREAM.INFO.KV_mesh-provisioner_registry", "$JS.API.STREAM.INFO.KV_postgres_bound", "$JS.API.STREAM.INFO.KV_route-adapter_bound", "$JS.API.STREAM.INFO.KV_route-proxy_bound", "$JS.API.STREAM.INFO.KV_node-tools_bound", "$JS.FC.KV_mesh-config_resolved.>", "$JS.FC.KV_mesh-controller_assignments.>", "$JS.FC.KV_mesh-provisioner_applied.>", "$JS.FC.KV_mesh-provisioner_collect.>", "$JS.FC.KV_mesh-provisioner_needs.>", "$JS.FC.KV_mesh-provisioner_registry.>", "$JS.FC.KV_postgres_bound.>", "$JS.FC.KV_route-adapter_bound.>", "$JS.FC.KV_route-proxy_bound.>", "$JS.FC.KV_node-tools_bound.>", "$KV.mesh-provisioner_applied.g14.>", "$KV.mesh-provisioner_collect.g14.>", "$KV.mesh-provisioner_needs.g14.>", "mesh.seat.mesh-vault.call.get.provisioner~g14", "prov.applied.g14.>", "prov.changed.g14.>"] }
|
||||
publish: { allow: ["$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.g14.>", "$JS.API.CONSUMER.CREATE.KV_mesh-controller_assignments.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_applied.*.$KV.mesh-provisioner_applied.g14.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_collect.*.$KV.mesh-provisioner_collect.g14.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_needs.*.$KV.mesh-provisioner_needs.g14.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_registry.>", "$JS.API.CONSUMER.CREATE.KV_node-engine_have.*.$KV.node-engine_have.g14.node", "$JS.API.CONSUMER.DELETE.KV_node-engine_have.>", "$JS.API.DIRECT.GET.KV_node-engine_have.$KV.node-engine_have.g14.node", "$JS.API.STREAM.INFO.KV_node-engine_have", "$JS.FC.KV_node-engine_have.>", "$JS.API.CONSUMER.CREATE.KV_postgres_bound.*.$KV.postgres_bound.g14.>", "$JS.API.CONSUMER.CREATE.KV_route-adapter_bound.*.$KV.route-adapter_bound.g14.>", "$JS.API.CONSUMER.CREATE.KV_route-proxy_bound.*.$KV.route-proxy_bound.g14.>", "$JS.API.CONSUMER.CREATE.KV_node-tools_bound.*.$KV.node-tools_bound.g14.>", "$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.>", "$JS.API.CONSUMER.DELETE.KV_mesh-controller_assignments.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_applied.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_collect.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_needs.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_registry.>", "$JS.API.CONSUMER.DELETE.KV_postgres_bound.>", "$JS.API.CONSUMER.DELETE.KV_route-adapter_bound.>", "$JS.API.CONSUMER.DELETE.KV_route-proxy_bound.>", "$JS.API.CONSUMER.DELETE.KV_node-tools_bound.>", "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.g14.>", "$JS.API.DIRECT.GET.KV_mesh-controller_assignments.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_applied.$KV.mesh-provisioner_applied.g14.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_collect.$KV.mesh-provisioner_collect.g14.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_needs.$KV.mesh-provisioner_needs.g14.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_registry.>", "$JS.API.DIRECT.GET.KV_postgres_bound.$KV.postgres_bound.g14.>", "$JS.API.DIRECT.GET.KV_route-adapter_bound.$KV.route-adapter_bound.g14.>", "$JS.API.DIRECT.GET.KV_route-proxy_bound.$KV.route-proxy_bound.g14.>", "$JS.API.DIRECT.GET.KV_node-tools_bound.$KV.node-tools_bound.g14.>", "$JS.API.STREAM.INFO.KV_mesh-config_resolved", "$JS.API.STREAM.INFO.KV_mesh-controller_assignments", "$JS.API.STREAM.INFO.KV_mesh-provisioner_applied", "$JS.API.STREAM.INFO.KV_mesh-provisioner_collect", "$JS.API.STREAM.INFO.KV_mesh-provisioner_needs", "$JS.API.STREAM.INFO.KV_mesh-provisioner_registry", "$JS.API.STREAM.INFO.KV_postgres_bound", "$JS.API.STREAM.INFO.KV_route-adapter_bound", "$JS.API.STREAM.INFO.KV_route-proxy_bound", "$JS.API.STREAM.INFO.KV_node-tools_bound", "$JS.FC.KV_mesh-config_resolved.>", "$JS.FC.KV_mesh-controller_assignments.>", "$JS.FC.KV_mesh-provisioner_applied.>", "$JS.FC.KV_mesh-provisioner_collect.>", "$JS.FC.KV_mesh-provisioner_needs.>", "$JS.FC.KV_mesh-provisioner_registry.>", "$JS.FC.KV_postgres_bound.>", "$JS.FC.KV_route-adapter_bound.>", "$JS.FC.KV_route-proxy_bound.>", "$JS.FC.KV_node-tools_bound.>", "$KV.mesh-provisioner_applied.g14.>", "$KV.mesh-provisioner_collect.g14.>", "$KV.mesh-provisioner_needs.g14.>", "mesh.seat.mesh-vault.call.get.provisioner~g14", "prov.applied.g14.>", "prov.changed.g14.>"] }
|
||||
subscribe: { allow: ["_INBOX.provisioner.g14.>", "mesh.mod.mesh-vault.event.secret-changed", "prov.ask.g14"] }
|
||||
allow_responses: { max: 1, ttl: "1m" }
|
||||
} }
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-provisioner-client
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021114014-66e8fdaa62e2
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024234636-10b6e202320f
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
golang.org/x/sys v0.48.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021114014-66e8fdaa62e2 h1:Cdl5N56QPfmFBoLHrDfvvyc5Q+PUnTjf3fgYD7imcco=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021114014-66e8fdaa62e2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024234636-10b6e202320f h1:lUEyNPPF3w4Q0LIgcUUD6ofkKVhCF0n7dVjMoixQmXo=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024234636-10b6e202320f/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
Executable
BIN
Binary file not shown.
Reference in New Issue
Block a user