Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
604ccd10d4 | ||
|
|
52d22f07d7 | ||
|
|
8f6a2b1f82 | ||
|
|
a0fbd6bcb5 | ||
|
|
5ce46cd197 | ||
|
|
8943a290f5 | ||
|
|
537e2ef31f | ||
|
|
545a172bbf | ||
|
|
391095ccb9 | ||
|
|
8536021f82 | ||
|
|
23307935b9 | ||
|
|
d5d6fd7412 | ||
|
|
2e69099354 | ||
|
|
5c65b1f3dc | ||
|
|
4f074e87c7 | ||
|
|
1968aba6e0 |
+303
@@ -34,3 +34,306 @@ modules/mesh-build-image/cmd/mesh-build-image/mesh-build-image
|
|||||||
/checks/paths/paths
|
/checks/paths/paths
|
||||||
/modules/mesh-build-go/mesh-build-go
|
/modules/mesh-build-go/mesh-build-go
|
||||||
/modules/mesh-delivery-gate/mesh-delivery-gate
|
/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.
@@ -0,0 +1,90 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
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,6 +49,51 @@ type FetchResult struct {
|
|||||||
// Beside are the sibling repositories placed as plain directories under the workspace's beside directory, which
|
// 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).
|
// build_go_test hands to the tests as MESH_CHECK_BESIDE (ADR 0484 clause 4).
|
||||||
Beside []Sibling `json:"beside,omitempty"`
|
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.
|
// fetchTool is the fetch step tool.
|
||||||
@@ -109,43 +154,77 @@ 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) {
|
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
|
repository, commit, ref, base := req.Repository, req.Commit, req.Ref, req.Base
|
||||||
var err error
|
var err error
|
||||||
|
took := &FetchTook{}
|
||||||
|
clock := newPhaseClock(m.now)
|
||||||
|
defer func() { took.Total = clock.total() }()
|
||||||
root := filepath.Join(m.Work, id)
|
root := filepath.Join(m.Work, id)
|
||||||
tree := filepath.Join(root, "tree")
|
tree := filepath.Join(root, "tree")
|
||||||
if err := os.MkdirAll(tree, 0o700); err != nil {
|
if err := os.MkdirAll(tree, 0o700); err != nil {
|
||||||
return nil, err
|
return FetchResult{}, err
|
||||||
}
|
}
|
||||||
remote := m.remote(repository)
|
remote := m.remote(repository)
|
||||||
if err := buildstep.FetchCommit(ctx, m.Run, tree, remote, commit, ref, m.fetchOptions(ctx, repository, commit, ref)...); err != nil {
|
opts := m.fetchOptions(ctx, repository, commit, ref)
|
||||||
return nil, err
|
mergeOpts := m.FetchOptions
|
||||||
}
|
|
||||||
res := FetchResult{Workspace: id, Commit: commit}
|
|
||||||
if base != "" {
|
if base != "" {
|
||||||
if err := m.merge(ctx, &res, tree, remote, base, commit); err != nil {
|
// The mirror is brought to the trunk commit too, so the merge preview deepens the tree from it and asks the forge
|
||||||
return nil, err
|
// for no history the mirror holds (issue 1921, try speed S6).
|
||||||
|
mergeOpts = m.fetchOptions(ctx, repository, base, "")
|
||||||
|
}
|
||||||
|
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}
|
||||||
|
if base != "" {
|
||||||
|
if err := m.merge(ctx, &res, tree, remote, base, commit, mergeOpts); err != nil {
|
||||||
|
return FetchResult{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
clock.lap(&took.Merge)
|
||||||
if err := m.dissociate(ctx, tree); err != nil {
|
if err := m.dissociate(ctx, tree); err != nil {
|
||||||
return nil, err
|
return FetchResult{}, err
|
||||||
}
|
}
|
||||||
|
clock.lap(&took.Dissociate)
|
||||||
if res.Beside, err = m.placeSiblings(ctx, root, siblings); err != nil {
|
if res.Beside, err = m.placeSiblings(ctx, root, siblings); err != nil {
|
||||||
return nil, err
|
return FetchResult{}, err
|
||||||
}
|
}
|
||||||
|
clock.lap(&took.Beside)
|
||||||
if (base == "" || res.Merge != "") && len(req.Paths) > 0 {
|
if (base == "" || res.Merge != "") && len(req.Paths) > 0 {
|
||||||
if err := buildstep.Narrow(tree, req); err != nil {
|
if err := buildstep.Narrow(tree, req); err != nil {
|
||||||
return nil, err
|
return FetchResult{}, err
|
||||||
}
|
}
|
||||||
// Said beside the tree, so the steps that list the tree's go.mod files leave out those narrowed away.
|
// 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 {
|
if err := os.WriteFile(filepath.Join(root, narrowedMark), nil, 0o600); err != nil {
|
||||||
return nil, err
|
return FetchResult{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dirs, err := m.goModDirs(ctx, tree)
|
dirs, err := m.goModDirs(ctx, tree)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return FetchResult{}, err
|
||||||
}
|
}
|
||||||
res.Modules = dirs
|
res.Modules = dirs
|
||||||
|
clock.lap(&took.Other)
|
||||||
if base != "" && res.Merge == "" {
|
if base != "" && res.Merge == "" {
|
||||||
return res, nil // a conflict: the tree is at the trunk, nothing of the head is built or tested
|
return res, nil // a conflict: the tree is at the trunk, nothing of the head is built or tested
|
||||||
}
|
}
|
||||||
@@ -168,9 +247,10 @@ func (m *Module) fetchInto(ctx context.Context, req buildstep.FetchRequest, id s
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
clock.lap(&took.Modules)
|
||||||
for _, err := range errs {
|
for _, err := range errs {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return FetchResult{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
@@ -293,6 +373,7 @@ func (m *Module) goEnv() []string {
|
|||||||
modcache = m.GoModCache
|
modcache = m.GoModCache
|
||||||
}
|
}
|
||||||
return []string{
|
return []string{
|
||||||
|
"CGO_ENABLED=0", // as a production build compiles (build.go), so a check tests what ships (issue 1921, S11)
|
||||||
"GOTOOLCHAIN=local",
|
"GOTOOLCHAIN=local",
|
||||||
"GOFLAGS=-mod=mod",
|
"GOFLAGS=-mod=mod",
|
||||||
"GOMODCACHE=" + modcache,
|
"GOMODCACHE=" + modcache,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -302,3 +303,61 @@ 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,8 +10,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
||||||
@@ -62,34 +64,99 @@ func (m *Module) integrationHere(ctx context.Context, workspace, tree string, pa
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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.
|
// Only the directories the selected packages lie in compile (ADR 0536 §3); no selection is every directory.
|
||||||
byDir, err := packagesByDir(tree, dirs, packages)
|
byDir, err := packagesByDir(tree, dirs, packages)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
for _, d := range dirs {
|
||||||
if len(byDir[d]) == 0 {
|
if len(byDir[d]) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
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]...)...)
|
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]...)...)
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
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)
|
setup := m.clock().Sub(began)
|
||||||
|
|
||||||
return m.runLevel(ctx, tree, levelOpts{
|
return m.runLevel(ctx, tree, levelOpts{
|
||||||
workspace: workspace, run: pattern,
|
workspace: workspace, run: pattern, runByDir: runByDir,
|
||||||
level: LevelIntegration, tags: integrationTags, packages: packages, bounds: m.integrationBounds(),
|
level: LevelIntegration, tags: integrationTags, packages: packages, bounds: m.integrationBounds(),
|
||||||
env: env,
|
env: env,
|
||||||
setup: setup, wall: true,
|
setup: setup, wall: true,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -35,6 +36,8 @@ type integFake struct {
|
|||||||
// command runs, with its line.
|
// command runs, with its line.
|
||||||
gomods string
|
gomods string
|
||||||
onTest func(line 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 {
|
func newIntegFake(t *testing.T, captured string) *integFake {
|
||||||
@@ -71,7 +74,10 @@ func (f *integFake) run(ctx context.Context, env []string, name string, args ...
|
|||||||
return forgekey.Ran{Stdout: f.gomods}
|
return forgekey.Ran{Stdout: f.gomods}
|
||||||
}
|
}
|
||||||
return forgekey.Ran{Stdout: "go.mod\n"}
|
return forgekey.Ran{Stdout: "go.mod\n"}
|
||||||
case name == "go" && strings.Contains(line, "-run ^$"):
|
case name == "go" && strings.Contains(line, "-exec true"):
|
||||||
|
if f.onCompile != nil {
|
||||||
|
f.onCompile(line)
|
||||||
|
}
|
||||||
f.tick("compile")
|
f.tick("compile")
|
||||||
case name == "go":
|
case name == "go":
|
||||||
if f.onTest != nil {
|
if f.onTest != nil {
|
||||||
@@ -123,9 +129,32 @@ func integModule(t *testing.T, f *integFake) (*Module, *keptStates) {
|
|||||||
if err := os.MkdirAll(filepath.Join(m.Work, "ws1", "tree"), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Join(m.Work, "ws1", "tree"), 0o755); err != nil {
|
||||||
t.Fatal(err)
|
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
|
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) {
|
func runLevelTool(t *testing.T, m *Module, states *keptStates, level string) (TestResult, delivery.Step) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
for _, tool := range m.Tools(m.StepTools()) {
|
for _, tool := range m.Tools(m.StepTools()) {
|
||||||
@@ -314,11 +343,11 @@ func TestAStoreThatDoesNotStartEndsTheStepFailedAndLeavesNothing(t *testing.T) {
|
|||||||
if !strings.Contains(f.lines(), " stop") {
|
if !strings.Contains(f.lines(), " stop") {
|
||||||
t.Errorf("the half-started store was not stopped:\n%s", f.lines())
|
t.Errorf("the half-started store was not stopped:\n%s", f.lines())
|
||||||
}
|
}
|
||||||
if f.busDir != "" {
|
if f.busDir != "" && !f.busDown {
|
||||||
t.Errorf("the bus was started after the store failed")
|
t.Errorf("the bus, started beside the store, was not stopped after the store failed")
|
||||||
}
|
}
|
||||||
if entries, _ := os.ReadDir(filepath.Join(m.Work, "ws1")); len(entries) != 1 || entries[0].Name() != "tree" {
|
if left := workspaceLeft(filepath.Join(m.Work, "ws1")); len(left) != 1 || left[0] != "tree" {
|
||||||
t.Errorf("the workspace holds %v, want only its tree", entries)
|
t.Errorf("the workspace holds %v, want only its tree", left)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,3 +406,138 @@ func TestTheSocketPathStaysUnderTheLimitForALongRunDirectory(t *testing.T) {
|
|||||||
t.Errorf("the short socket directory %s outlived the store (%v)", sock, err)
|
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,21 +29,30 @@ 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,
|
// 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 a throwaway layer unless write, and an environment of a PATH and
|
// its packed artifacts) read-write, the caches beneath the ask's own layer unless write, and an environment of a PATH and
|
||||||
// a locale alone (the step's HOME is its own, inside its /tmp).
|
// a locale alone (the step's HOME is its own, inside its /tmp).
|
||||||
func (m *Module) isolation(workspace string, write bool) buildstep.Isolation {
|
func (m *Module) isolation(workspace string, write bool) buildstep.Isolation {
|
||||||
path := os.Getenv("PATH")
|
path := os.Getenv("PATH")
|
||||||
if path == "" {
|
if path == "" {
|
||||||
path = defaultPath
|
path = defaultPath
|
||||||
}
|
}
|
||||||
return buildstep.Isolation{
|
iso := buildstep.Isolation{
|
||||||
Workspace: filepath.Join(m.Work, workspace),
|
Workspace: filepath.Join(m.Work, workspace),
|
||||||
Caches: m.caches(),
|
Caches: m.caches(),
|
||||||
WriteCaches: write,
|
WriteCaches: write,
|
||||||
Env: []string{"PATH=" + path, "LC_ALL=C"},
|
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
|
// 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
|
// 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.
|
// integration level's own process, already running shut off (level.go): it runs its commands as they are.
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
type shutOff struct {
|
type shutOff struct {
|
||||||
flags []string
|
flags []string
|
||||||
binds, roBinds, overlays []string
|
binds, roBinds, overlays []string
|
||||||
|
uppers map[string]string // the writable layer each --overlay gives a cache: cache -> upper
|
||||||
env []string
|
env []string
|
||||||
command string
|
command string
|
||||||
chdir string
|
chdir string
|
||||||
@@ -27,7 +28,7 @@ type shutOff struct {
|
|||||||
|
|
||||||
// bwrapArity is how many words each bubblewrap option the SDK writes takes.
|
// 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,
|
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,
|
"--proc": 1, "--dev": 1, "--tmpfs": 1, "--dir": 1, "--chdir": 1, "--overlay-src": 1, "--tmp-overlay": 1, "--overlay": 3,
|
||||||
"--ro-bind": 2, "--ro-bind-try": 2, "--bind": 2, "--symlink": 2, "--setenv": 2}
|
"--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
|
// throughBubblewrap is the fake runner of a step's tests: a bubblewrap command is read back (recorded in seen) and its
|
||||||
@@ -56,6 +57,11 @@ func throughBubblewrap(t *testing.T, run forgekey.Runner, seen *[]shutOff) forge
|
|||||||
s.roBinds = append(s.roBinds, args[i+1])
|
s.roBinds = append(s.roBinds, args[i+1])
|
||||||
case "--overlay-src":
|
case "--overlay-src":
|
||||||
s.overlays = append(s.overlays, args[i+1])
|
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":
|
case "--setenv":
|
||||||
s.env = append(s.env, args[i+1]+"="+args[i+2])
|
s.env = append(s.env, args[i+1]+"="+args[i+2])
|
||||||
case "--chdir":
|
case "--chdir":
|
||||||
@@ -105,6 +111,9 @@ 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)
|
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):
|
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)
|
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) {
|
if want := filepath.Join(m.Work, "ws1"); !slices.Contains(s.binds, want) {
|
||||||
@@ -131,7 +140,8 @@ func TestTheUnitLevelRunsEveryGoTestShutOff(t *testing.T) {
|
|||||||
t.Fatalf("%d go commands ran, %d of them shut off: %v", ran, len(seen), *calls)
|
t.Fatalf("%d go commands ran, %d of them shut off: %v", ran, len(seen), *calls)
|
||||||
}
|
}
|
||||||
for _, s := range seen {
|
for _, s := range seen {
|
||||||
checkShutOff(t, m, s, false)
|
// 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 "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ type levelSpec struct {
|
|||||||
Run string `json:"run,omitempty"`
|
Run string `json:"run,omitempty"`
|
||||||
Setup time.Duration `json:"setup"`
|
Setup time.Duration `json:"setup"`
|
||||||
Bound time.Duration `json:"bound"`
|
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.
|
// levelAnswer is the one line the process prints: the level's result or the digest, or why it could not run.
|
||||||
@@ -83,7 +85,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
|
// 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.
|
// module's toolchains read-only for the integration level.
|
||||||
func (m *Module) shutOff(ctx context.Context, exe string, spec levelSpec) (levelAnswer, error) {
|
func (m *Module) shutOff(ctx context.Context, exe string, spec levelSpec) (levelAnswer, error) {
|
||||||
spec.Work, spec.Cache = m.Work, m.Cache
|
spec.Work, spec.Cache, spec.Parallel = m.Work, m.Cache, m.parallel()
|
||||||
raw, err := json.Marshal(spec)
|
raw, err := json.Marshal(spec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return levelAnswer{}, err
|
return levelAnswer{}, err
|
||||||
@@ -165,7 +167,7 @@ func runLevelProcess(args []string) int {
|
|||||||
return say(levelAnswer{Error: fmt.Sprintf("%q is not a workspace id", spec.Workspace)})
|
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,
|
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}, Say: func(string, ...any) {}}
|
IntegrationBounds: Bounds{Setup: spec.Setup, Run: spec.Bound}, Parallel: spec.Parallel, Say: func(string, ...any) {}}
|
||||||
m.Run = StepRunner(m.Home)
|
m.Run = StepRunner(m.Home)
|
||||||
return say(m.answer(context.Background(), spec))
|
return say(m.answer(context.Background(), spec))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,8 +57,14 @@ type Failure struct {
|
|||||||
Test string `json:"test,omitempty"`
|
Test string `json:"test,omitempty"`
|
||||||
Output string `json:"output,omitempty"`
|
Output string `json:"output,omitempty"`
|
||||||
Cause string `json:"cause,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.
|
// TestResult is what a test step passes with.
|
||||||
type TestResult struct {
|
type TestResult struct {
|
||||||
Level string `json:"level"`
|
Level string `json:"level"`
|
||||||
@@ -73,7 +79,7 @@ type TestResult struct {
|
|||||||
Elapsed float64 `json:"elapsed"`
|
Elapsed float64 `json:"elapsed"`
|
||||||
// Setup is the level's setup time in seconds, when it has a setup.
|
// Setup is the level's setup time in seconds, when it has a setup.
|
||||||
Setup float64 `json:"setup,omitempty"`
|
Setup float64 `json:"setup,omitempty"`
|
||||||
// Lint is the unit level's gofmt and go vet time in seconds, before its tests (ADR 0529 §1).
|
// Lint is the unit level's gofmt and go vet time in seconds, run beside its tests (ADR 0529 §1; issue 1921).
|
||||||
Lint float64 `json:"lint,omitempty"`
|
Lint float64 `json:"lint,omitempty"`
|
||||||
// Bound and SetupBound are the bounds the level was judged by, in seconds.
|
// Bound and SetupBound are the bounds the level was judged by, in seconds.
|
||||||
Bound float64 `json:"bound"`
|
Bound float64 `json:"bound"`
|
||||||
|
|||||||
@@ -60,17 +60,23 @@ 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, and the build tags they name.
|
// unformatted are the Go files of a directory gofmt would change, relative to the tree, the build tags the directory's
|
||||||
func unformatted(tree, d string) (files, tags []string, err error) {
|
// 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) {
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
err = lintable(filepath.Join(tree, d), func(path string) error {
|
base := filepath.Join(tree, d)
|
||||||
|
err = lintable(base, func(path string) error {
|
||||||
src, err := os.ReadFile(path)
|
src, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if out, ferr := format.Source(src); ferr != nil || !bytes.Equal(out, src) {
|
pkgDir, _ := filepath.Rel(base, filepath.Dir(path))
|
||||||
rel, _ := filepath.Rel(tree, path)
|
if only == nil || only[filepath.ToSlash(pkgDir)] {
|
||||||
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))
|
scan := bufio.NewScanner(bytes.NewReader(src))
|
||||||
for scan.Scan() {
|
for scan.Scan() {
|
||||||
@@ -79,8 +85,15 @@ func unformatted(tree, d string) (files, tags []string, err error) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
if expr, ok := strings.CutPrefix(line, "//go:build "); ok {
|
if expr, ok := strings.CutPrefix(line, "//go:build "); ok {
|
||||||
for _, tag := range buildTag.FindAllString(expr, -1) {
|
for _, at := range buildTag.FindAllStringIndex(expr, -1) {
|
||||||
if !notATag[tag] && !strings.HasPrefix(tag, "go1") && !seen[tag] {
|
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] {
|
||||||
seen[tag] = true
|
seen[tag] = true
|
||||||
tags = append(tags, tag)
|
tags = append(tags, tag)
|
||||||
}
|
}
|
||||||
@@ -91,62 +104,123 @@ func unformatted(tree, d string) (files, tags []string, err error) {
|
|||||||
})
|
})
|
||||||
sort.Strings(files)
|
sort.Strings(files)
|
||||||
sort.Strings(tags)
|
sort.Strings(tags)
|
||||||
return files, tags, err
|
return files, tags, negated, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// boundOf is a bound of n processes of this process alone.
|
||||||
// failure of either is a Failure of the level naming what failed.
|
func boundOf(n int) func(context.Context) (func(), error) {
|
||||||
func (m *Module) lint(ctx context.Context, run forgekey.Runner, tree, d string, env []string) ([]Failure, error) {
|
slots := make(chan struct{}, n)
|
||||||
files, tags, err := unformatted(tree, d)
|
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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("reading the Go files of %s: %w", d, err)
|
return nil, fmt.Errorf("reading the Go files of %s: %w", d, err)
|
||||||
}
|
}
|
||||||
var failed []Failure
|
var failed []Failure
|
||||||
if len(files) > 0 {
|
if len(files) > 0 {
|
||||||
failed = append(failed, Failure{Package: d, Test: "gofmt",
|
failed = append(failed, Failure{Package: d, Test: "gofmt", Step: StepLint,
|
||||||
Output: "not as gofmt writes them (run gofmt -w): " + strings.Join(files, ", ")})
|
Output: "not as gofmt writes them (run gofmt -w): " + strings.Join(files, ", ")})
|
||||||
}
|
}
|
||||||
sets := [][]string{nil}
|
sets := [][]string{tags}
|
||||||
if len(tags) > 0 {
|
if len(tags) > 0 && negated {
|
||||||
sets = append(sets, tags)
|
sets = [][]string{nil, tags}
|
||||||
}
|
}
|
||||||
for _, set := range sets {
|
type vetRun struct {
|
||||||
|
name string
|
||||||
|
r forgekey.Ran
|
||||||
|
}
|
||||||
|
runs := make([]vetRun, len(sets))
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for i, set := range sets {
|
||||||
args := []string{"-C", filepath.Join(tree, d), "vet"}
|
args := []string{"-C", filepath.Join(tree, d), "vet"}
|
||||||
name := "go vet"
|
name := "go vet"
|
||||||
if len(set) > 0 {
|
if len(set) > 0 {
|
||||||
args = append(args, "-tags", strings.Join(set, ","))
|
args = append(args, "-tags", strings.Join(set, ","))
|
||||||
name += " -tags " + strings.Join(set, ",")
|
name += " -tags " + strings.Join(set, ",")
|
||||||
}
|
}
|
||||||
args = append(args, "./...")
|
args = append(args, targets...)
|
||||||
r := run(ctx, env, "go", args...)
|
runs[i].name = name
|
||||||
if r.Err != "" {
|
wg.Add(1)
|
||||||
return nil, fmt.Errorf("%s in %s could not be run: %s", name, d, firstLine(r.Err+"\n"+r.Stderr))
|
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))
|
||||||
}
|
}
|
||||||
if r.Status != 0 {
|
if v.r.Status != 0 {
|
||||||
failed = append(failed, Failure{Package: d, Test: name, Output: strings.TrimSpace(r.Stderr + "\n" + r.Stdout)})
|
failed = append(failed, Failure{Package: d, Test: v.name, Step: StepLint, Output: strings.TrimSpace(v.r.Stderr + "\n" + v.r.Stdout)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return failed, nil
|
return failed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// lintAll lints each directory, up to m.parallel() at once like the level's tests; the failures join in directory order
|
// 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).
|
// and the error is the first directory's, so the answer does not depend on which ended first (issue 1930). byDir names
|
||||||
func (m *Module) lintAll(ctx context.Context, run forgekey.Runner, tree string, dirs []string, env func(d string) []string) ([]Failure, error) {
|
// 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) {
|
||||||
type answer struct {
|
type answer struct {
|
||||||
failed []Failure
|
failed []Failure
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
answers := make([]answer, len(dirs))
|
answers := make([]answer, len(dirs))
|
||||||
slots := make(chan struct{}, m.parallel())
|
if acquire == nil {
|
||||||
|
acquire = boundOf(m.parallel())
|
||||||
|
}
|
||||||
var wg sync.WaitGroup
|
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 crash any // a panic of a lint goes on in the caller, as a panic of a test run does
|
||||||
var crashMu sync.Mutex
|
var crashMu sync.Mutex
|
||||||
for i, d := range dirs {
|
for i, d := range dirs {
|
||||||
|
release, err := acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
answers[i].err = err
|
||||||
|
break
|
||||||
|
}
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
slots <- struct{}{}
|
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
defer func() { <-slots }()
|
defer release()
|
||||||
defer func() {
|
defer func() {
|
||||||
if p := recover(); p != nil {
|
if p := recover(); p != nil {
|
||||||
crashMu.Lock()
|
crashMu.Lock()
|
||||||
@@ -154,7 +228,7 @@ func (m *Module) lintAll(ctx context.Context, run forgekey.Runner, tree string,
|
|||||||
crashMu.Unlock()
|
crashMu.Unlock()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
answers[i].failed, answers[i].err = m.lint(ctx, run, tree, d, env(d))
|
answers[i].failed, answers[i].err = m.lint(ctx, run, tree, d, env(d), byDir[d])
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,8 +30,9 @@ 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
|
// 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 no tag and with every tag the files name together, integration and isolation included, so
|
// alone), and go vet runs with every tag the files name together, integration and isolation included, so a vet error
|
||||||
// a vet error behind isolation fails the level naming the run.
|
// 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.
|
||||||
func TestTheUnitLevelRunsGofmtAndVetWithEveryTag(t *testing.T) {
|
func TestTheUnitLevelRunsGofmtAndVetWithEveryTag(t *testing.T) {
|
||||||
tree := writeTree(t, map[string]string{
|
tree := writeTree(t, map[string]string{
|
||||||
"go.mod": "module x\n\ngo 1.26\n",
|
"go.mod": "module x\n\ngo 1.26\n",
|
||||||
@@ -51,11 +54,11 @@ func TestTheUnitLevelRunsGofmtAndVetWithEveryTag(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return forgekey.Ran{}
|
return forgekey.Ran{}
|
||||||
}
|
}
|
||||||
failed, err := (&Module{}).lint(context.Background(), run, tree, ".", nil)
|
failed, err := (&Module{}).lint(context.Background(), run, tree, ".", nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if strings.Join(vetted, " | ") != "vet ./... | vet -tags integration,isolation ./..." {
|
if strings.Join(vetted, " | ") != "vet -tags integration,isolation ./..." {
|
||||||
t.Errorf("vet ran %v", vetted)
|
t.Errorf("vet ran %v", vetted)
|
||||||
}
|
}
|
||||||
if len(failed) != 2 || failed[0].Test != "gofmt" || !strings.Contains(failed[0].Output, "b.go") ||
|
if len(failed) != 2 || failed[0].Test != "gofmt" || !strings.Contains(failed[0].Output, "b.go") ||
|
||||||
@@ -67,14 +70,14 @@ func TestTheUnitLevelRunsGofmtAndVetWithEveryTag(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// A formatted directory that vets passes.
|
// A formatted directory that vets passes.
|
||||||
clean := writeTree(t, map[string]string{"go.mod": "module y\n", "a.go": "package y\n"})
|
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); 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, nil); err != nil || len(failed) != 0 {
|
||||||
t.Errorf("a clean directory: %+v, %v", failed, err)
|
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.
|
// 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) {
|
func TestAnUnformattedFileFailsTheUnitLevel(t *testing.T) {
|
||||||
m, states, ws, calls := workspaceFor(t, "empty")
|
m, states, ws, _ := workspaceFor(t, "empty")
|
||||||
tree := filepath.Join(m.Work, ws, "tree")
|
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"} {
|
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 {
|
if err := os.WriteFile(filepath.Join(tree, name), []byte(body), 0o644); err != nil {
|
||||||
@@ -85,50 +88,117 @@ 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") {
|
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)
|
t.Fatalf("result = %+v, want the level failed naming x.go", res)
|
||||||
}
|
}
|
||||||
// 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") {
|
// 1921/S4: the lint runs beside the unit level's tests, not before them: both have started before either ends, a lint
|
||||||
t.Errorf("the tests ran after gofmt failed: %s", c.line)
|
// 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issue 1930: the lint of many directories runs side by side up to the level's bound, not one after another, and the
|
// 1921/S3: the lint of a level that runs some packages vets those packages alone, in one pass with every tag their
|
||||||
// failures still join in directory order.
|
// directory names, and reports a gofmt fault only in their files: a fault in a package the level keeps was reported
|
||||||
func TestTheLintOfManyDirectoriesRunsAtOnceUpToTheBound(t *testing.T) {
|
// when that package changed.
|
||||||
files := map[string]string{}
|
func TestTheLintOfSomePackagesVetsThemInOnePassAndFormatsOnlyTheirFiles(t *testing.T) {
|
||||||
var dirs []string
|
tree := writeTree(t, map[string]string{
|
||||||
for _, d := range []string{"a", "b", "c", "d", "e", "f"} {
|
"go.mod": "module x\n\ngo 1.26\n",
|
||||||
files[d+"/go.mod"] = "module x/" + d + "\n"
|
"a/a.go": "package a\n\nfunc A() {}\n",
|
||||||
files[d+"/x.go"] = "package x\n"
|
"a/a_integration_test.go": "//go:build integration\n\npackage a\n",
|
||||||
dirs = append(dirs, d)
|
"b/b.go": "package b\nfunc B() {}\n",
|
||||||
}
|
"a/deeper/d.go": "package deeper\nfunc D() {}\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 {
|
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{}
|
return forgekey.Ran{}
|
||||||
}
|
}
|
||||||
m := &Module{Parallel: 3}
|
failed, err := (&Module{}).lint(context.Background(), run, tree, ".", nil, []string{"x/a"})
|
||||||
failed, err := m.lintAll(context.Background(), run, tree, dirs, func(string) []string { return nil })
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if most < 2 || most > 3 {
|
if strings.Join(vetted, " | ") != "vet -tags integration x/a" {
|
||||||
t.Errorf("%d lint commands ran at once, want 2 or 3 (the bound)", most)
|
t.Errorf("vet ran %v, want one pass over x/a with its tag", vetted)
|
||||||
}
|
}
|
||||||
if len(failed) != 2 || failed[0].Package != "b" || failed[1].Package != "e" {
|
if len(failed) != 0 {
|
||||||
t.Errorf("failed = %+v, want b's then e's", failed)
|
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",
|
||||||
|
})
|
||||||
|
var mu sync.Mutex
|
||||||
|
var vetted []string
|
||||||
|
run := func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
|
||||||
|
mu.Lock()
|
||||||
|
vetted = append(vetted, strings.Join(args[2:], " "))
|
||||||
|
mu.Unlock()
|
||||||
|
return forgekey.Ran{}
|
||||||
|
}
|
||||||
|
if _, err := (&Module{}).lint(context.Background(), run, tree, ".", nil, nil); 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
"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 {
|
func fetchOnce(t *testing.T, m *Module, states *keptStates, args map[string]any) FetchResult {
|
||||||
@@ -188,3 +189,61 @@ func TestTheWorkspaceIsSelfContainedAfterAFetchThroughTheMirror(t *testing.T) {
|
|||||||
t.Errorf("the workspace is not whole: %v %s", err, out)
|
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,6 +13,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
||||||
)
|
)
|
||||||
@@ -34,8 +35,9 @@ func (m *Module) remote(repository string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// merge makes the merge preview of the head onto base and writes the answer into res.
|
// merge makes the merge preview of the head onto base and writes the answer into res.
|
||||||
func (m *Module) merge(ctx context.Context, res *FetchResult, tree, remote, base, commit string) error {
|
// opts are the fetch's options, with the mirror when it holds base.
|
||||||
mr, err := buildstep.MergeInto(ctx, m.Run, tree, remote, base, commit, m.FetchOptions...)
|
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...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -89,23 +91,22 @@ 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
|
// 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.
|
// 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) {
|
func (m *Module) placeSiblings(ctx context.Context, root string, siblings []siblingArg) ([]Sibling, error) {
|
||||||
var placed []Sibling
|
// The siblings are placed side by side (issue 1921, try speed S6); the first failure in the order given is the step's.
|
||||||
for _, s := range siblings {
|
placed := make([]Sibling, len(siblings))
|
||||||
name := s.repository[strings.Index(s.repository, "/")+1:]
|
errs := make([]error, len(siblings))
|
||||||
dir := filepath.Join(root, "beside", name)
|
var wg sync.WaitGroup
|
||||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
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 {
|
||||||
return nil, err
|
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 })
|
sort.Slice(placed, func(i, j int) bool { return placed[i].Dir < placed[j].Dir })
|
||||||
if len(placed) > 0 {
|
if len(placed) > 0 {
|
||||||
@@ -121,6 +122,25 @@ func (m *Module) placeSiblings(ctx context.Context, root string, siblings []sibl
|
|||||||
return placed, nil
|
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.
|
// besideRecord is the file in a workspace's root naming the siblings the fetch placed beside it.
|
||||||
const besideRecord = "beside.json"
|
const besideRecord = "beside.json"
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,8 @@ func TestFetchWithABaseMergesAndAnswersThePreview(t *testing.T) {
|
|||||||
t.Errorf("trees = %+v", res.Trees)
|
t.Errorf("trees = %+v", res.Trees)
|
||||||
}
|
}
|
||||||
lines := strings.Join(f.lines(), "\n")
|
lines := strings.Join(f.lines(), "\n")
|
||||||
for _, want := range []string{"fetch --no-tags --unshallow origin " + baseSHA, "checkout -q --detach " + baseSHA,
|
mirror := filepath.Join(m.Cache, "mirror", "novox", "mesh-sdk.git")
|
||||||
|
for _, want := range []string{"fetch --no-tags --unshallow " + mirror + " " + baseSHA, "checkout -q --detach " + baseSHA,
|
||||||
"core.hooksPath=/dev/null", "merge --no-ff --no-edit --no-verify"} {
|
"core.hooksPath=/dev/null", "merge --no-ff --no-edit --no-verify"} {
|
||||||
if !strings.Contains(lines, want) {
|
if !strings.Contains(lines, want) {
|
||||||
t.Errorf("git was not asked %q:\n%s", want, lines)
|
t.Errorf("git was not asked %q:\n%s", want, lines)
|
||||||
@@ -290,8 +291,8 @@ func TestTheIntegrationLevelIsGivenTheBesideDirectoryOnlyWhenSiblingsWerePlaced(
|
|||||||
}
|
}
|
||||||
tested := 0
|
tested := 0
|
||||||
for _, c := range f.calls {
|
for _, c := range f.calls {
|
||||||
if !strings.HasPrefix(c.line, "go ") || !strings.Contains(c.line, "-tags integration") {
|
if !strings.HasPrefix(c.line, "go ") || !strings.Contains(c.line, "-tags integration") || strings.Contains(c.line, "-exec true") {
|
||||||
continue
|
continue // the setup's compile runs no test binary, so it is given nothing a test reads (issue 1921, S5)
|
||||||
}
|
}
|
||||||
tested++
|
tested++
|
||||||
got := ""
|
got := ""
|
||||||
|
|||||||
@@ -24,9 +24,7 @@ func threeModules(t *testing.T, f *integFake) (*Module, *keptStates) {
|
|||||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module fixture/"+d+"\n\ngo 1.25.0\n"), 0o644); err != nil {
|
writeTaggedTest(t, dir, "fixture/"+d)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return m, states
|
return m, states
|
||||||
}
|
}
|
||||||
@@ -72,7 +70,7 @@ func TestOnlyTheSelectedDirectoryCompilesAndRuns(t *testing.T) {
|
|||||||
if !strings.HasPrefix(l, "go -C "+filepath.Join(tree, "b")+" test ") {
|
if !strings.HasPrefix(l, "go -C "+filepath.Join(tree, "b")+" test ") {
|
||||||
t.Errorf("go test ran outside the selected directory: %s", l)
|
t.Errorf("go test ran outside the selected directory: %s", l)
|
||||||
}
|
}
|
||||||
if strings.Contains(l, "-run ^$") {
|
if strings.Contains(l, "-exec true") {
|
||||||
compiled++
|
compiled++
|
||||||
} else {
|
} else {
|
||||||
ran++
|
ran++
|
||||||
@@ -91,7 +89,7 @@ func TestWithoutASelectionEveryDirectoryCompilesAndRuns(t *testing.T) {
|
|||||||
compiled, ran := 0, 0
|
compiled, ran := 0, 0
|
||||||
for _, l := range strings.Split(f.lines(), "\n") {
|
for _, l := range strings.Split(f.lines(), "\n") {
|
||||||
if strings.HasPrefix(l, "go ") && strings.Contains(l, " test ") {
|
if strings.HasPrefix(l, "go ") && strings.Contains(l, " test ") {
|
||||||
if strings.Contains(l, "-run ^$") {
|
if strings.Contains(l, "-exec true") {
|
||||||
compiled++
|
compiled++
|
||||||
} else {
|
} else {
|
||||||
ran++
|
ran++
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
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,7 +10,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -171,6 +170,9 @@ type levelOpts struct {
|
|||||||
// parallelism, where the unit level's is the slowest package's.
|
// parallelism, where the unit level's is the slowest package's.
|
||||||
setup time.Duration
|
setup time.Duration
|
||||||
wall bool
|
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).
|
// compose is a state of the level to put in the result (the store and the bus it ran against).
|
||||||
store, bus string
|
store, bus string
|
||||||
}
|
}
|
||||||
@@ -205,35 +207,56 @@ func (m *Module) runLevel(ctx context.Context, tree string, o levelOpts) (*TestR
|
|||||||
todo = append(todo, d)
|
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 {
|
if o.level == LevelUnit {
|
||||||
// gofmt and go vet of each directory the level runs come before its tests (ADR 0529 §1; issue 1887 D7-go): a
|
// gofmt and go vet of each directory the level runs, beside its tests (ADR 0529 §1; issue 1887 D7-go; issue 1921,
|
||||||
// directory that fails them fails the level at once, and their time is the result's Lint.
|
// try speed S4): a directory that fails them fails the level, and their time is the result's Lint.
|
||||||
linted := m.clock()
|
lintDone = make(chan struct{})
|
||||||
failed, err := m.lintAll(ctx, run, tree, todo, func(d string) []string {
|
go func() {
|
||||||
return append(m.goEnvFor(filepath.Join(tree, d)), o.env...)
|
defer close(lintDone)
|
||||||
})
|
defer func() {
|
||||||
if err != nil {
|
if p := recover(); p != nil {
|
||||||
return nil, err
|
crashMu.Lock()
|
||||||
}
|
crash = p
|
||||||
res.Lint = m.clock().Sub(linted).Seconds()
|
crashMu.Unlock()
|
||||||
if len(failed) > 0 {
|
}
|
||||||
res.Failed = failed
|
}()
|
||||||
res.judge(o.bounds)
|
linted := m.clock()
|
||||||
return res, nil
|
// 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
|
||||||
began = m.clock() // the level's running time is its tests'
|
// 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()
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
runs := make([]dirRun, len(todo))
|
runs := make([]dirRun, len(todo))
|
||||||
slots := make(chan struct{}, m.parallel())
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
var crash any // a panic of a run goes on in the caller, which cleans up after it
|
var slotErr error
|
||||||
var crashMu sync.Mutex
|
|
||||||
for i, d := range todo {
|
for i, d := range todo {
|
||||||
|
release, err := slots.acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
slotErr = err
|
||||||
|
break
|
||||||
|
}
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
slots <- struct{}{}
|
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
defer func() { <-slots }()
|
defer release()
|
||||||
defer func() {
|
defer func() {
|
||||||
if p := recover(); p != nil {
|
if p := recover(); p != nil {
|
||||||
crashMu.Lock()
|
crashMu.Lock()
|
||||||
@@ -245,7 +268,9 @@ func (m *Module) runLevel(ctx context.Context, tree string, o levelOpts) (*TestR
|
|||||||
if len(o.tags) > 0 {
|
if len(o.tags) > 0 {
|
||||||
args = append(args, "-tags", strings.Join(o.tags, ","))
|
args = append(args, "-tags", strings.Join(o.tags, ","))
|
||||||
}
|
}
|
||||||
if o.run != "" {
|
if r := o.runByDir[d]; r != "" {
|
||||||
|
args = append(args, "-run", r)
|
||||||
|
} else if o.run != "" {
|
||||||
args = append(args, "-run", o.run)
|
args = append(args, "-run", o.run)
|
||||||
}
|
}
|
||||||
args = append(args, byDir[d]...)
|
args = append(args, byDir[d]...)
|
||||||
@@ -258,9 +283,19 @@ func (m *Module) runLevel(ctx context.Context, tree string, o levelOpts) (*TestR
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
if lintDone != nil {
|
||||||
|
<-lintDone
|
||||||
|
}
|
||||||
if crash != nil {
|
if crash != nil {
|
||||||
panic(crash)
|
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 {
|
for i, d := range todo {
|
||||||
dr := runs[i]
|
dr := runs[i]
|
||||||
if (dr.r.Status != 0 || dr.r.Err != "") && len(dr.rd.pkgs) == 0 {
|
if (dr.r.Status != 0 || dr.r.Err != "") && len(dr.rd.pkgs) == 0 {
|
||||||
@@ -286,12 +321,20 @@ func (m *Module) runLevel(ctx context.Context, tree string, o levelOpts) (*TestR
|
|||||||
// runAnswer is what a step runner answers.
|
// runAnswer is what a step runner answers.
|
||||||
type runAnswer = forgekey.Ran
|
type runAnswer = forgekey.Ran
|
||||||
|
|
||||||
// parallel is how many directories a level runs at once.
|
// 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.
|
||||||
func (m *Module) parallel() int {
|
func (m *Module) parallel() int {
|
||||||
if m.Parallel > 0 {
|
if m.Parallel > 0 {
|
||||||
return m.Parallel
|
return m.Parallel
|
||||||
}
|
}
|
||||||
return max(1, runtime.NumCPU()/2)
|
capacity := 0
|
||||||
|
if m.Capacity != nil {
|
||||||
|
capacity = m.Capacity()
|
||||||
|
}
|
||||||
|
if capacity <= 0 {
|
||||||
|
capacity = 2
|
||||||
|
}
|
||||||
|
return max(1, numCPU()/capacity)
|
||||||
}
|
}
|
||||||
|
|
||||||
// packagesByDir says which patterns run in which go.mod directory. A relative pattern (./...) runs in every directory, as
|
// packagesByDir says which patterns run in which go.mod directory. A relative pattern (./...) runs in every directory, as
|
||||||
|
|||||||
@@ -66,7 +66,9 @@ func TestUnitLevelRunsTheRealGoOnTheFixtureRepositories(t *testing.T) {
|
|||||||
"tagged": {passed: true, noTests: true},
|
"tagged": {passed: true, noTests: true},
|
||||||
"empty": {passed: true, noTests: true},
|
"empty": {passed: true, noTests: true},
|
||||||
"slow": {passed: true},
|
"slow": {passed: true},
|
||||||
"broken": {failed: "."}, // go vet, before the tests since R4g (ADR 0529 §1), names the directory and the compiler's line
|
// 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: "."},
|
||||||
}
|
}
|
||||||
for name, w := range want {
|
for name, w := range want {
|
||||||
home := t.TempDir()
|
home := t.TempDir()
|
||||||
@@ -84,14 +86,16 @@ func TestUnitLevelRunsTheRealGoOnTheFixtureRepositories(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%s: %v", name, err)
|
t.Fatalf("%s: %v", name, err)
|
||||||
}
|
}
|
||||||
if res.Passed != w.passed || res.NoTests != w.noTests || (w.failed != "" && (len(res.Failed) != 1 || res.Failed[0].Package != w.failed)) {
|
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)) {
|
||||||
t.Errorf("%s: result = %+v", name, res)
|
t.Errorf("%s: result = %+v", name, res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1887/D7-go against the real go: a tree holding an unformatted file fails the unit level naming the file, before any
|
// 1887/D7-go against the real go: a tree holding an unformatted file fails the check naming the file, as the lint step's
|
||||||
// test runs, and the level's result carries the lint's time.
|
// failure (ADR 0542 §2), and the level's result carries the lint's time.
|
||||||
func TestAnUnformattedFileFailsTheRealUnitLevel(t *testing.T) {
|
func TestAnUnformattedFileFailsTheRealUnitLevel(t *testing.T) {
|
||||||
needTools(t, "the unit level's gofmt and vet", "go", "git", "bwrap")
|
needTools(t, "the unit level's gofmt and vet", "go", "git", "bwrap")
|
||||||
home := t.TempDir()
|
home := t.TempDir()
|
||||||
@@ -111,8 +115,9 @@ func TestAnUnformattedFileFailsTheRealUnitLevel(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if res.Passed || len(res.Failed) == 0 || res.Failed[0].Test != "gofmt" || !strings.Contains(res.Failed[0].Output, "ugly.go") || res.Tests != 0 {
|
if res.Passed || len(res.Failed) == 0 || res.Failed[0].Test != "gofmt" || res.Failed[0].Step != StepLint ||
|
||||||
t.Fatalf("result = %+v, want the level failed by gofmt naming ugly.go and no test run", res)
|
!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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,9 +161,8 @@ func TestIntegrationLevelRunsAgainstARealStoreAndBusAndLeavesNothing(t *testing.
|
|||||||
if !res.Passed || res.Tests != 1 || res.Setup <= 0 || res.Setup > IntegrationSetup.Seconds() {
|
if !res.Passed || res.Tests != 1 || res.Setup <= 0 || res.Setup > IntegrationSetup.Seconds() {
|
||||||
t.Errorf("result = %+v", res)
|
t.Errorf("result = %+v", res)
|
||||||
}
|
}
|
||||||
entries, _ := os.ReadDir(filepath.Join(work, "ws1"))
|
if left := workspaceLeft(filepath.Join(work, "ws1")); len(left) != 1 || left[0] != "tree" {
|
||||||
if len(entries) != 1 || entries[0].Name() != "tree" {
|
t.Errorf("the workspace holds %v besides the ask's own directories, want only its tree: the run directory is gone", left)
|
||||||
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) {
|
for deadline := time.Now().Add(5 * time.Second); len(processesNamed(work)) > 0 && time.Now().Before(deadline); time.Sleep(100 * time.Millisecond) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -25,8 +26,11 @@ func capturedRunner(t *testing.T, captured string, calls *[]cmdRecord) forgekey.
|
|||||||
if strings.Contains(string(raw), `"Action":"fail"`) {
|
if strings.Contains(string(raw), `"Action":"fail"`) {
|
||||||
status = 1 // go test exits 1 when a test or a build failed
|
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 {
|
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, " ")})
|
*calls = append(*calls, cmdRecord{env: env, line: name + " " + strings.Join(args, " ")})
|
||||||
|
mu.Unlock()
|
||||||
switch {
|
switch {
|
||||||
case name == "git" && args[2] == "ls-files":
|
case name == "git" && args[2] == "ls-files":
|
||||||
return forgekey.Ran{Stdout: "go.mod\n"}
|
return forgekey.Ran{Stdout: "go.mod\n"}
|
||||||
@@ -103,6 +107,33 @@ 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
|
// 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.
|
// seconds): the level fails naming the bound and the slowest tests; under the real bound the same run passes.
|
||||||
func TestUnitLevelOverItsBoundFailsNamingTheBoundAndTheSlowestTests(t *testing.T) {
|
func TestUnitLevelOverItsBoundFailsNamingTheBoundAndTheSlowestTests(t *testing.T) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
Each file is the standard output of `GOTOOLCHAIN=go1.27.1 GOFLAGS=-mod=mod go test -count=1 -json ./...` (go1.27.1)
|
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
|
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`.
|
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.
|
||||||
|
|
||||||
golist-external.json is `GOTOOLCHAIN=go1.27.1 GOFLAGS=-mod=mod go list -json=ImportPath,Standard,DepOnly,Module
|
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
|
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
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{"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}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module fixture/twofailing
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package one
|
||||||
|
|
||||||
|
func One() int { return 1 }
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package two
|
||||||
|
|
||||||
|
func Two() int { return 2 }
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
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
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024125710-bfc3883af980
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024232922-99407c37df95
|
||||||
github.com/distribution/distribution/v3 v3.1.2
|
github.com/distribution/distribution/v3 v3.1.2
|
||||||
github.com/nats-io/nats-server/v2 v2.11.17
|
github.com/nats-io/nats-server/v2 v2.11.17
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
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.20261024214154-bc16740feb0d h1:oJREw0ggKqJWJ5DEISSL0i2ZC5XqdXAidxo9NJ74TCc=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024125710-bfc3883af980/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
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=
|
||||||
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
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/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=
|
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
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023213200-403a5d72ab93
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261024232922-99407c37df95
|
||||||
github.com/distribution/distribution/v3 v3.1.2
|
github.com/distribution/distribution/v3 v3.1.2
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
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.20261024214154-bc16740feb0d h1:oJREw0ggKqJWJ5DEISSL0i2ZC5XqdXAidxo9NJ74TCc=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023213200-403a5d72ab93/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
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=
|
||||||
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
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/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=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user