Add end-to-end integration test with fake external binaries

Exercises the full run() pipeline using fake shell scripts on PATH that
record their argv to a temp RECORD_DIR. Verifies all external commands
were invoked, outputs contain the expected version/app/artifact values,
ditto was called twice (pre- and post-staple), and security delete-keychain
ran during cleanup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leon Mika 2026-05-02 10:37:12 +10:00
parent 445c99e6c4
commit 22aa4d6069
10 changed files with 170 additions and 0 deletions

View file

@ -0,0 +1,117 @@
package main
import (
"context"
"encoding/base64"
"os"
"path/filepath"
"strings"
"testing"
)
func TestRun_EndToEnd_FakeBinaries(t *testing.T) {
if testing.Short() {
t.Skip("integration test skipped in -short mode")
}
repoRoot, _ := os.Getwd() // cmd/wails-release
binDir := filepath.Join(repoRoot, "testdata", "bin")
sampleSrc := filepath.Join(repoRoot, "testdata", "sample-app")
// Copy the sample app to a writable temp dir so the build step can
// drop output into it without mutating testdata.
work := t.TempDir()
copyTree(t, sampleSrc, work)
record := t.TempDir()
outputs := filepath.Join(record, "outputs")
must(t, os.WriteFile(outputs, nil, 0o600))
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
t.Setenv("RECORD_DIR", record)
t.Setenv("WORK_DIR", work)
t.Setenv("GITHUB_OUTPUT", outputs)
t.Setenv("GITHUB_REF", "refs/tags/v1.2.3")
t.Setenv("GITHUB_SHA", "0123456789abcdef")
for k, v := range map[string]string{
"INPUT_WORKING_DIRECTORY": work,
"INPUT_DEVELOPER_ID_CERT_BASE64": base64.StdEncoding.EncodeToString([]byte("not-a-real-p12")),
"INPUT_DEVELOPER_ID_CERT_PASSWORD": "x",
"INPUT_NOTARIZATION_METHOD": "api-key",
"INPUT_NOTARIZATION_API_KEY_BASE64": base64.StdEncoding.EncodeToString([]byte("not-a-real-p8")),
"INPUT_NOTARIZATION_API_KEY_ID": "K",
"INPUT_NOTARIZATION_API_ISSUER_ID": "I",
} {
t.Setenv(k, v)
}
if err := run(context.Background()); err != nil {
t.Fatalf("run: %v", err)
}
// Each external command must have been called.
for _, name := range []string{"wails.log", "security.log", "codesign.log", "ditto.log", "xcrun.log"} {
path := filepath.Join(record, name)
b, err := os.ReadFile(path)
if err != nil {
t.Fatalf("expected %s, got error: %v", name, err)
}
if len(b) == 0 {
t.Fatalf("%s was empty", name)
}
}
// Outputs file should contain version=1.2.3.
out, err := os.ReadFile(outputs)
if err != nil {
t.Fatalf("read outputs: %v", err)
}
if !strings.Contains(string(out), "version=1.2.3") {
t.Fatalf("expected version=1.2.3 in outputs, got %q", out)
}
if !strings.Contains(string(out), "app-name=SampleApp") {
t.Fatalf("expected app-name=SampleApp in outputs, got %q", out)
}
if !strings.Contains(string(out), "artifact-filename=SampleApp-1.2.3.app.zip") {
t.Fatalf("expected artifact-filename in outputs, got %q", out)
}
// ditto must have been invoked twice (pre-staple + post-staple).
dittoLog, _ := os.ReadFile(filepath.Join(record, "ditto.log"))
if strings.Count(string(dittoLog), "\n") < 2 {
t.Fatalf("expected ditto called at least twice, log:\n%s", dittoLog)
}
// security delete-keychain must have run during cleanup.
secLog, _ := os.ReadFile(filepath.Join(record, "security.log"))
if !strings.Contains(string(secLog), "delete-keychain") {
t.Fatalf("expected security delete-keychain in cleanup, log:\n%s", secLog)
}
}
func must(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatal(err)
}
}
func copyTree(t *testing.T, src, dst string) {
t.Helper()
must(t, filepath.Walk(src, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
rel, _ := filepath.Rel(src, p)
out := filepath.Join(dst, rel)
if info.IsDir() {
return os.MkdirAll(out, 0o755)
}
b, err := os.ReadFile(p)
if err != nil {
return err
}
return os.WriteFile(out, b, info.Mode())
}))
}

3
cmd/wails-release/testdata/bin/codesign vendored Executable file
View file

@ -0,0 +1,3 @@
#!/bin/bash
echo "$0 $*" >> "$RECORD_DIR/codesign.log"
exit 0

7
cmd/wails-release/testdata/bin/ditto vendored Executable file
View file

@ -0,0 +1,7 @@
#!/bin/bash
echo "$0 $*" >> "$RECORD_DIR/ditto.log"
# Last argument is the output path; create it.
out="${@: -1}"
mkdir -p "$(dirname "$out")"
echo "fake-zip" > "$out"
exit 0

9
cmd/wails-release/testdata/bin/go vendored Executable file
View file

@ -0,0 +1,9 @@
#!/bin/bash
echo "$0 $*" >> "$RECORD_DIR/go.log"
# install: pretend success.
# env GOPATH: print a tmp value (not actually used in the test path).
case "$1" in
install) ;;
env) echo "/tmp" ;;
esac
exit 0

6
cmd/wails-release/testdata/bin/security vendored Executable file
View file

@ -0,0 +1,6 @@
#!/bin/bash
echo "$0 $*" >> "$RECORD_DIR/security.log"
if [[ "$1" == "find-identity" ]]; then
echo ' 1) AAAA1111 "Developer ID Application: Acme Inc (TEAM1234)"'
fi
exit 0

9
cmd/wails-release/testdata/bin/wails vendored Executable file
View file

@ -0,0 +1,9 @@
#!/bin/bash
echo "$0 $*" >> "$RECORD_DIR/wails.log"
case "$1" in
-v) echo "Wails CLI v2.11.0" ;;
build)
mkdir -p "$WORK_DIR/build/bin/SampleApp.app/Contents/MacOS"
touch "$WORK_DIR/build/bin/SampleApp.app/Contents/MacOS/SampleApp"
;;
esac

12
cmd/wails-release/testdata/bin/xcrun vendored Executable file
View file

@ -0,0 +1,12 @@
#!/bin/bash
echo "$0 $*" >> "$RECORD_DIR/xcrun.log"
case "$1" in
notarytool)
case "$2" in
submit) echo '{"id":"sub-1","status":"Accepted"}' ;;
log) echo '{}' ;;
esac
;;
stapler) ;;
esac
exit 0

View file

@ -0,0 +1,5 @@
module example.com/sample
go 1.22.0
require github.com/wailsapp/wails/v2 v2.11.0

View file

@ -0,0 +1 @@
github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k=

View file

@ -0,0 +1 @@
{ "name": "SampleApp" }