mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
ci(nix): unify cached verification and strengthen packaged smoke
Run deterministic installer and publisher checks in Nix, align dev and release dependency artifacts, narrow derivation inputs, and use content-aware CI cache keys. Replace duplicate Cargo gates with a Nix pre-push gate; validate JSON streams and cache data in representative packaged-binary smoke cases.
This commit is contained in:
parent
b4dcfceef6
commit
26ffd445e8
6 changed files with 252 additions and 162 deletions
44
.github/workflows/ci.yml
vendored
44
.github/workflows/ci.yml
vendored
|
|
@ -5,12 +5,15 @@ on:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Everything that compiles Rust runs as a `nix flake check` derivation
|
# The same complete offline gate is used locally and in CI.
|
||||||
# (fmt, clippy, tests, package verify, build, offline smoke). Crane splits
|
|
||||||
# the crate dependency build into its own derivation, and the Nix store is
|
|
||||||
# restored from the GitHub Actions cache, so unchanged derivations are
|
|
||||||
# substituted instead of rebuilt.
|
|
||||||
nix:
|
nix:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
|
@ -24,44 +27,27 @@ jobs:
|
||||||
- name: Restore and save Nix store
|
- name: Restore and save Nix store
|
||||||
uses: nix-community/cache-nix-action@v7
|
uses: nix-community/cache-nix-action@v7
|
||||||
with:
|
with:
|
||||||
primary-key: nix-${{ runner.os }}-${{ hashFiles('flake.nix', 'flake.lock', 'Cargo.lock', 'Cargo.toml') }}
|
primary-key: nix-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('flake.nix', 'flake.lock', 'Cargo.lock', 'Cargo.toml') }}-${{ hashFiles('src/**', 'tests/**', 'scripts/**', 'install.sh', 'README.md', 'LICENSE') }}
|
||||||
restore-prefixes-first-match: nix-${{ runner.os }}-
|
restore-prefixes-first-match: |
|
||||||
|
nix-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('flake.nix', 'flake.lock', 'Cargo.lock', 'Cargo.toml') }}-
|
||||||
|
nix-v2-${{ runner.os }}-${{ runner.arch }}-
|
||||||
gc-max-store-size-linux: 8G
|
gc-max-store-size-linux: 8G
|
||||||
|
|
||||||
- name: Flake check
|
- name: Flake check
|
||||||
run: nix flake check -L
|
run: nix flake check -L
|
||||||
|
|
||||||
- name: Run default app
|
# Keep native BSD/macOS tools in the installer portability check; the Linux
|
||||||
run: nix run .#default -- version
|
# sh/dash scenarios and ShellCheck are covered by the flake.
|
||||||
|
|
||||||
- name: Publish ownership snapshot tests
|
|
||||||
run: scripts/publish-ownership-snapshot-test.sh
|
|
||||||
|
|
||||||
- name: Build ownership publisher
|
|
||||||
run: |
|
|
||||||
nix build .#ownership-publisher
|
|
||||||
./result/bin/idx-ownership-publish --help
|
|
||||||
|
|
||||||
install-script:
|
install-script:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-14]
|
os: [macos-14]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: ShellCheck
|
|
||||||
if: runner.os == 'Linux'
|
|
||||||
run: |
|
|
||||||
shellcheck -s sh install.sh
|
|
||||||
shellcheck scripts/install-sh-test.sh
|
|
||||||
|
|
||||||
- name: Test install.sh with sh
|
- name: Test install.sh with sh
|
||||||
run: scripts/install-sh-test.sh
|
run: scripts/install-sh-test.sh
|
||||||
|
|
||||||
- name: Test install.sh with dash
|
|
||||||
if: runner.os == 'Linux'
|
|
||||||
run: INSTALL_SH_SHELL=dash scripts/install-sh-test.sh
|
|
||||||
|
|
|
||||||
|
|
@ -7,24 +7,10 @@ repos:
|
||||||
|
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
- id: cargo-fmt
|
- id: nix-flake-check
|
||||||
name: cargo fmt
|
name: nix flake check
|
||||||
entry: cargo fmt --check
|
entry: nix flake check -L
|
||||||
language: system
|
language: system
|
||||||
types: [rust]
|
|
||||||
pass_filenames: false
|
|
||||||
|
|
||||||
- id: cargo-clippy
|
|
||||||
name: cargo clippy
|
|
||||||
entry: cargo clippy -- -D warnings
|
|
||||||
language: system
|
|
||||||
types: [rust]
|
|
||||||
pass_filenames: false
|
|
||||||
|
|
||||||
- id: cargo-test
|
|
||||||
name: cargo test
|
|
||||||
entry: cargo nextest run
|
|
||||||
language: system
|
|
||||||
types: [rust]
|
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
|
always_run: true
|
||||||
stages: [pre-push]
|
stages: [pre-push]
|
||||||
|
|
|
||||||
95
flake.nix
95
flake.nix
|
|
@ -26,14 +26,18 @@
|
||||||
curl-impersonate
|
curl-impersonate
|
||||||
mupdf
|
mupdf
|
||||||
];
|
];
|
||||||
# Only the crate itself (manifests, src/, tests/), so doc or script
|
# Keep integration-test edits out of the installed package's inputs.
|
||||||
# edits do not invalidate the Rust derivations.
|
# Fixtures stay here because provider code embeds some of them.
|
||||||
rustFiles = [
|
rustFiles = [
|
||||||
./Cargo.toml
|
./Cargo.toml
|
||||||
./Cargo.lock
|
./Cargo.lock
|
||||||
./src
|
./src
|
||||||
./tests
|
./tests/fixtures
|
||||||
];
|
];
|
||||||
|
testSrc = lib.fileset.toSource {
|
||||||
|
root = ./.;
|
||||||
|
fileset = lib.fileset.unions (rustFiles ++ [ ./tests ]);
|
||||||
|
};
|
||||||
src = lib.fileset.toSource {
|
src = lib.fileset.toSource {
|
||||||
root = ./.;
|
root = ./.;
|
||||||
fileset = lib.fileset.unions rustFiles;
|
fileset = lib.fileset.unions rustFiles;
|
||||||
|
|
@ -42,15 +46,21 @@
|
||||||
inherit src;
|
inherit src;
|
||||||
pname = cargoManifest.package.name;
|
pname = cargoManifest.package.name;
|
||||||
version = cargoManifest.package.version;
|
version = cargoManifest.package.version;
|
||||||
|
CARGO_PROFILE = "dev";
|
||||||
strictDeps = true;
|
strictDeps = true;
|
||||||
nativeBuildInputs = with pkgs; [ pkg-config ];
|
nativeBuildInputs = with pkgs; [ pkg-config ];
|
||||||
buildInputs = with pkgs; [ openssl ];
|
buildInputs = with pkgs; [ openssl ];
|
||||||
};
|
};
|
||||||
# Dependency-only build keyed on Cargo.toml/Cargo.lock: reused from the
|
# Checks and cargo package verification use dev artifacts; the shipped
|
||||||
# Nix store (or binary cache) until the dependency graph changes.
|
# application has a separate release dependency cache.
|
||||||
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
cargoArtifacts = craneLib.buildDepsOnly (commonArgs // { src = testSrc; });
|
||||||
|
releaseCargoArtifacts = craneLib.buildDepsOnly (commonArgs // {
|
||||||
|
CARGO_PROFILE = "release";
|
||||||
|
doCheck = false;
|
||||||
|
});
|
||||||
idxPackage = craneLib.buildPackage (commonArgs // {
|
idxPackage = craneLib.buildPackage (commonArgs // {
|
||||||
inherit cargoArtifacts;
|
cargoArtifacts = releaseCargoArtifacts;
|
||||||
|
CARGO_PROFILE = "release";
|
||||||
doCheck = false;
|
doCheck = false;
|
||||||
nativeBuildInputs = commonArgs.nativeBuildInputs ++ [ pkgs.makeWrapper ];
|
nativeBuildInputs = commonArgs.nativeBuildInputs ++ [ pkgs.makeWrapper ];
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
|
|
@ -106,6 +116,30 @@
|
||||||
'';
|
'';
|
||||||
meta.mainProgram = "idx-ownership-publish";
|
meta.mainProgram = "idx-ownership-publish";
|
||||||
};
|
};
|
||||||
|
installerSrc = lib.fileset.toSource {
|
||||||
|
root = ./.;
|
||||||
|
fileset = lib.fileset.unions [ ./install.sh ./scripts/install-sh-test.sh ];
|
||||||
|
};
|
||||||
|
installerCheck = shell: pkgs.runCommand "idx-installer-${shell}" {
|
||||||
|
nativeBuildInputs = with pkgs; [
|
||||||
|
bash coreutils curl dash findutils gnugrep gnused gawk python3
|
||||||
|
];
|
||||||
|
} ''
|
||||||
|
cp -r ${installerSrc}/. .
|
||||||
|
chmod -R u+w scripts
|
||||||
|
patchShebangs scripts
|
||||||
|
export HOME="$TMPDIR"
|
||||||
|
INSTALL_SH_SHELL=${shell} bash scripts/install-sh-test.sh
|
||||||
|
touch "$out"
|
||||||
|
'';
|
||||||
|
publisherTestSrc = lib.fileset.toSource {
|
||||||
|
root = ./.;
|
||||||
|
fileset = lib.fileset.unions [
|
||||||
|
./scripts/publish-ownership-snapshot-test.sh
|
||||||
|
./scripts/publish-ownership-snapshot.sh
|
||||||
|
./scripts/build-ownership-snapshot.sh
|
||||||
|
];
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
packages.default = idxPackage;
|
packages.default = idxPackage;
|
||||||
|
|
@ -117,14 +151,23 @@
|
||||||
};
|
};
|
||||||
checks = {
|
checks = {
|
||||||
default = idxPackage;
|
default = idxPackage;
|
||||||
ownership-publisher = ownershipPublisher;
|
ownership-publisher = pkgs.runCommand "idx-ownership-publisher-smoke" { } ''
|
||||||
fmt = craneLib.cargoFmt { inherit src; };
|
${ownershipPublisher}/bin/idx-ownership-publish --help
|
||||||
|
touch "$out"
|
||||||
|
'';
|
||||||
|
fmt = craneLib.cargoFmt { src = testSrc; };
|
||||||
clippy = craneLib.cargoClippy (commonArgs // {
|
clippy = craneLib.cargoClippy (commonArgs // {
|
||||||
inherit cargoArtifacts;
|
inherit cargoArtifacts;
|
||||||
|
src = testSrc;
|
||||||
|
doInstallCargoArtifacts = false;
|
||||||
|
installPhaseCommand = "touch $out";
|
||||||
cargoClippyExtraArgs = "--all-targets -- -D warnings";
|
cargoClippyExtraArgs = "--all-targets -- -D warnings";
|
||||||
});
|
});
|
||||||
test = craneLib.cargoTest (commonArgs // {
|
test = craneLib.cargoTest (commonArgs // {
|
||||||
inherit cargoArtifacts;
|
inherit cargoArtifacts;
|
||||||
|
src = testSrc;
|
||||||
|
doInstallCargoArtifacts = false;
|
||||||
|
installPhaseCommand = "touch $out";
|
||||||
# Ownership discover tests point IDX_CURL_IMPERSONATE_BIN at plain
|
# Ownership discover tests point IDX_CURL_IMPERSONATE_BIN at plain
|
||||||
# `curl` against a local fixture server.
|
# `curl` against a local fixture server.
|
||||||
nativeCheckInputs = [ pkgs.curl ];
|
nativeCheckInputs = [ pkgs.curl ];
|
||||||
|
|
@ -137,6 +180,7 @@
|
||||||
src = lib.fileset.toSource {
|
src = lib.fileset.toSource {
|
||||||
root = ./.;
|
root = ./.;
|
||||||
fileset = lib.fileset.unions (rustFiles ++ [
|
fileset = lib.fileset.unions (rustFiles ++ [
|
||||||
|
./tests
|
||||||
./LICENSE
|
./LICENSE
|
||||||
./README.md
|
./README.md
|
||||||
]);
|
]);
|
||||||
|
|
@ -146,11 +190,12 @@
|
||||||
installPhaseCommand = "touch $out";
|
installPhaseCommand = "touch $out";
|
||||||
});
|
});
|
||||||
smoke-mock = pkgs.runCommand "idx-smoke-mock"
|
smoke-mock = pkgs.runCommand "idx-smoke-mock"
|
||||||
{ nativeBuildInputs = with pkgs; [ bash coreutils findutils gnugrep gnused gawk ]; }
|
{ nativeBuildInputs = with pkgs; [ bash coreutils diffutils findutils gnugrep gnused gawk jq ]; }
|
||||||
''
|
''
|
||||||
cp -r ${./scripts} scripts
|
mkdir -p scripts
|
||||||
chmod -R u+w scripts
|
cp ${./scripts/live-smoke.sh} scripts/live-smoke.sh
|
||||||
patchShebangs scripts
|
chmod u+w scripts/live-smoke.sh
|
||||||
|
patchShebangs scripts/live-smoke.sh
|
||||||
# The mock provider reads fixtures relative to the working dir.
|
# The mock provider reads fixtures relative to the working dir.
|
||||||
mkdir -p tests
|
mkdir -p tests
|
||||||
cp -r ${./tests/fixtures} tests/fixtures
|
cp -r ${./tests/fixtures} tests/fixtures
|
||||||
|
|
@ -159,6 +204,28 @@
|
||||||
bash scripts/live-smoke.sh --bin ${idxPackage}/bin/idx --no-build --mode mock
|
bash scripts/live-smoke.sh --bin ${idxPackage}/bin/idx --no-build --mode mock
|
||||||
touch "$out"
|
touch "$out"
|
||||||
'';
|
'';
|
||||||
|
install-script-sh = installerCheck "sh";
|
||||||
|
install-script-dash = installerCheck "dash";
|
||||||
|
shellcheck = pkgs.runCommand "idx-shellcheck" {
|
||||||
|
nativeBuildInputs = [ pkgs.shellcheck ];
|
||||||
|
} ''
|
||||||
|
shellcheck -s sh ${installerSrc}/install.sh
|
||||||
|
shellcheck ${installerSrc}/scripts/install-sh-test.sh
|
||||||
|
shellcheck ${./scripts/live-smoke.sh}
|
||||||
|
touch "$out"
|
||||||
|
'';
|
||||||
|
publisher-test = pkgs.runCommand "idx-publisher-test" {
|
||||||
|
nativeBuildInputs = with pkgs; [
|
||||||
|
bash coreutils findutils gawk gnugrep gnused jq sqlite
|
||||||
|
];
|
||||||
|
} ''
|
||||||
|
cp -r ${publisherTestSrc}/. .
|
||||||
|
chmod -R u+w scripts
|
||||||
|
patchShebangs scripts
|
||||||
|
export HOME="$TMPDIR"
|
||||||
|
bash scripts/publish-ownership-snapshot-test.sh
|
||||||
|
touch "$out"
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
|
|
@ -169,6 +236,8 @@
|
||||||
cargo-watch
|
cargo-watch
|
||||||
cargo-nextest
|
cargo-nextest
|
||||||
prek
|
prek
|
||||||
|
jq
|
||||||
|
diffutils
|
||||||
] ++ runtimeDeps;
|
] ++ runtimeDeps;
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ set -Eeuo pipefail
|
||||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
installer="${repo_root}/install.sh"
|
installer="${repo_root}/install.sh"
|
||||||
test_shell="${INSTALL_SH_SHELL:-sh}"
|
test_shell="${INSTALL_SH_SHELL:-sh}"
|
||||||
|
fixture_shell="$(command -v sh)"
|
||||||
work="$(mktemp -d "${TMPDIR:-/tmp}/idx-install-test.XXXXXX")"
|
work="$(mktemp -d "${TMPDIR:-/tmp}/idx-install-test.XXXXXX")"
|
||||||
server_pid=
|
server_pid=
|
||||||
|
|
||||||
|
|
@ -73,7 +74,7 @@ EOF
|
||||||
make_release() {
|
make_release() {
|
||||||
local tag="$1" dir="${root}/gh/0xrsydn/idx-cli/releases/download/$1"
|
local tag="$1" dir="${root}/gh/0xrsydn/idx-cli/releases/download/$1"
|
||||||
mkdir -p "$dir"
|
mkdir -p "$dir"
|
||||||
printf '#!/bin/sh\necho "idx fake %s"\n' "$tag" >"${dir}/${asset}"
|
printf '#!%s\necho "idx fake %s"\n' "$fixture_shell" "$tag" >"${dir}/${asset}"
|
||||||
chmod +x "${dir}/${asset}"
|
chmod +x "${dir}/${asset}"
|
||||||
(cd "$dir" && echo "$(sha256 "$asset") ${asset}" >SHA256SUMS)
|
(cd "$dir" && echo "$(sha256 "$asset") ${asset}" >SHA256SUMS)
|
||||||
}
|
}
|
||||||
|
|
@ -83,7 +84,7 @@ make_release v0.2.4
|
||||||
# v0.0.9: checksum does not match the binary.
|
# v0.0.9: checksum does not match the binary.
|
||||||
bad="${root}/gh/0xrsydn/idx-cli/releases/download/v0.0.9"
|
bad="${root}/gh/0xrsydn/idx-cli/releases/download/v0.0.9"
|
||||||
mkdir -p "$bad"
|
mkdir -p "$bad"
|
||||||
printf '#!/bin/sh\necho tampered\n' >"${bad}/${asset}"
|
printf '#!%s\necho tampered\n' "$fixture_shell" >"${bad}/${asset}"
|
||||||
printf '%064d %s\n' 0 "$asset" >"${bad}/SHA256SUMS"
|
printf '%064d %s\n' 0 "$asset" >"${bad}/SHA256SUMS"
|
||||||
|
|
||||||
port="$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1])')"
|
port="$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1])')"
|
||||||
|
|
@ -151,7 +152,7 @@ fi
|
||||||
shim="${work}/shim"
|
shim="${work}/shim"
|
||||||
mkdir -p "$shim"
|
mkdir -p "$shim"
|
||||||
# shellcheck disable=SC2016 # literal $1 belongs to the shim
|
# shellcheck disable=SC2016 # literal $1 belongs to the shim
|
||||||
printf '#!/bin/sh\ncase "$1" in -s) echo FreeBSD ;; *) echo amd64 ;; esac\n' >"${shim}/uname"
|
printf '#!%s\ncase "$1" in -s) echo FreeBSD ;; *) echo amd64 ;; esac\n' "$fixture_shell" >"${shim}/uname"
|
||||||
chmod +x "${shim}/uname"
|
chmod +x "${shim}/uname"
|
||||||
if run_installer env PATH="${shim}:${PATH}" "$test_shell" "$installer" --dir "${work}/bsd" >"${work}/out6" 2>&1; then
|
if run_installer env PATH="${shim}:${PATH}" "$test_shell" "$installer" --dir "${work}/bsd" >"${work}/out6" 2>&1; then
|
||||||
fail "rejects unsupported OS (installer succeeded)"
|
fail "rejects unsupported OS (installer succeeded)"
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ Groups:
|
||||||
general version/completions/config/cache basics
|
general version/completions/config/cache basics
|
||||||
live-table live network stock commands in table mode
|
live-table live network stock commands in table mode
|
||||||
live-json live network stock commands in JSON mode
|
live-json live network stock commands in JSON mode
|
||||||
mock mock-backed stock commands in table + JSON mode
|
mock mock-backed stock JSON contracts + representative table checks
|
||||||
cache deterministic cache/offline/stale-cache checks
|
cache deterministic cache/offline/stale-cache checks
|
||||||
routing provider routing and explicit unsupported checks
|
routing provider routing and explicit unsupported checks
|
||||||
errors JSON error contract and invalid-flag checks
|
errors JSON error contract and invalid-flag checks
|
||||||
|
|
@ -139,7 +139,7 @@ should_run_group() {
|
||||||
}
|
}
|
||||||
|
|
||||||
add_case() {
|
add_case() {
|
||||||
CASES+=("$1$SEP$2$SEP$3$SEP$4$SEP$5$SEP$6")
|
CASES+=("$1$SEP$2$SEP$3$SEP$4$SEP$5$SEP$6$SEP${7:-}")
|
||||||
}
|
}
|
||||||
|
|
||||||
register_cases() {
|
register_cases() {
|
||||||
|
|
@ -154,19 +154,13 @@ register_cases() {
|
||||||
local screen_args="stocks screen --filter top-performers --limit $SCREEN_LIMIT"
|
local screen_args="stocks screen --filter top-performers --limit $SCREEN_LIMIT"
|
||||||
local news_args="stocks news $live_symbol --limit $NEWS_LIMIT"
|
local news_args="stocks news $live_symbol --limit $NEWS_LIMIT"
|
||||||
|
|
||||||
add_case "general" "version" "0" "" "version" ""
|
add_case "general" "version" "0" "" "-o json version" "" '.version | type == "string" and test("^[0-9]+\\.[0-9]+\\.[0-9]+")'
|
||||||
add_case "general" "completions-bash" "0" "" "completions bash" ""
|
add_case "general" "completions-bash" "0" "" "completions bash" ""
|
||||||
add_case "general" "completions-zsh" "0" "" "completions zsh" ""
|
|
||||||
add_case "general" "completions-fish" "0" "" "completions fish" ""
|
|
||||||
add_case "general" "config-path" "0" "" "config path" ""
|
|
||||||
add_case "general" "config-init" "0" "" "config init" ""
|
|
||||||
add_case "general" "config-set-output-json" "0" "" "config set general.output json" ""
|
add_case "general" "config-set-output-json" "0" "" "config set general.output json" ""
|
||||||
add_case "general" "config-get-output" "0" "" "config get general.output" "json"
|
add_case "general" "config-get-output" "0" "" "config get general.output" "json"
|
||||||
add_case "general" "config-set-output-table" "0" "" "config set general.output table" ""
|
add_case "general" "config-set-output-table" "0" "" "config set general.output table" ""
|
||||||
add_case "general" "config-set-db-path" "0" "" "config set ownership.db_path $OWNERSHIP_DB" ""
|
add_case "general" "cache-info" "0" "" "-o json cache info" "" '.files | type == "number" and . >= 0'
|
||||||
add_case "general" "config-get-db-path" "0" "" "config get ownership.db_path" "$OWNERSHIP_DB"
|
add_case "general" "cache-clear" "0" "" "-o json cache clear" "" '.removed | type == "number" and . >= 0'
|
||||||
add_case "general" "cache-info" "0" "" "cache info" ""
|
|
||||||
add_case "general" "cache-clear" "0" "" "cache clear" ""
|
|
||||||
|
|
||||||
add_case "live-table" "quote" "0" "$live_env" "stocks quote $live_symbol" ""
|
add_case "live-table" "quote" "0" "$live_env" "stocks quote $live_symbol" ""
|
||||||
add_case "live-table" "history" "0" "$live_env" "$history_args" ""
|
add_case "live-table" "history" "0" "$live_env" "$history_args" ""
|
||||||
|
|
@ -200,64 +194,51 @@ register_cases() {
|
||||||
add_case "live-json" "news" "0" "$live_env" "-o json $news_args" ""
|
add_case "live-json" "news" "0" "$live_env" "-o json $news_args" ""
|
||||||
add_case "live-json" "screen" "0" "$live_env" "-o json $screen_args" ""
|
add_case "live-json" "screen" "0" "$live_env" "-o json $screen_args" ""
|
||||||
|
|
||||||
add_case "live-nonfinite" "valuation-bumi" "0" "$live_env" "-o json stocks valuation BUMI" "\"overall_signal\""
|
add_case "live-nonfinite" "valuation-bumi" "0" "$live_env" "-o json stocks valuation BUMI" ""
|
||||||
add_case "live-nonfinite" "valuation-adro" "0" "$live_env" "-o json stocks valuation ADRO" "\"overall_signal\""
|
add_case "live-nonfinite" "valuation-adro" "0" "$live_env" "-o json stocks valuation ADRO" ""
|
||||||
add_case "live-nonfinite" "valuation-aims" "0" "$live_env" "-o json stocks valuation AIMS" "\"overall_signal\""
|
add_case "live-nonfinite" "valuation-aims" "0" "$live_env" "-o json stocks valuation AIMS" ""
|
||||||
add_case "live-nonfinite" "compare-bad-tickers" "0" "$live_env" "-o json stocks compare BUMI,ADRO,AIMS" "\"symbol\": \"BUMI.JK\""
|
add_case "live-nonfinite" "compare-bad-tickers" "0" "$live_env" "-o json stocks compare BUMI,ADRO,AIMS" "" 'any(.[]; .symbol == "BUMI.JK")'
|
||||||
|
|
||||||
add_case "mock" "quote-table" "0" "$mock_env" "stocks quote $live_symbol" ""
|
add_case "mock" "quote-table" "0" "$mock_env" "stocks quote $live_symbol" "9,875"
|
||||||
add_case "mock" "quote-json" "0" "$mock_env" "-o json stocks quote $live_symbol" ""
|
add_case "mock" "quote-json" "0" "$mock_env" "-o json stocks quote $live_symbol" "" '.[0].price == 9875 and .[0].change == 117'
|
||||||
add_case "mock" "history-table" "0" "$mock_env" "$history_args" ""
|
add_case "mock" "history-table" "0" "$mock_env" "$history_args" "2024-03-03"
|
||||||
add_case "mock" "history-json" "0" "$mock_env" "-o json $history_args" ""
|
add_case "mock" "history-json" "0" "$mock_env" "-o json $history_args" "" '.[-1].close == 9925'
|
||||||
add_case "mock" "technical-table" "0" "$mock_env" "stocks technical $live_symbol" ""
|
add_case "mock" "technical-json" "0" "$mock_env" "-o json stocks technical $live_symbol" "" '.current_price == 9925 and .sma200 == null'
|
||||||
add_case "mock" "technical-json" "0" "$mock_env" "-o json stocks technical $live_symbol" ""
|
add_case "mock" "growth-json" "0" "$mock_env" "-o json stocks growth $live_symbol" "" '.revenue_growth_pct == 8.1 and .earnings_growth_pct == 12.1'
|
||||||
add_case "mock" "growth-table" "0" "$mock_env" "stocks growth $live_symbol" ""
|
add_case "mock" "valuation-json" "0" "$mock_env" "-o json stocks valuation $live_symbol" "" '.pe_trailing == 25.4 and .roe_pct == 19.8'
|
||||||
add_case "mock" "growth-json" "0" "$mock_env" "-o json stocks growth $live_symbol" ""
|
add_case "mock" "risk-json" "0" "$mock_env" "-o json stocks risk $live_symbol" "" '.debt_to_equity == 0.42 and .roa_pct == 3.1'
|
||||||
add_case "mock" "valuation-table" "0" "$mock_env" "stocks valuation $live_symbol" ""
|
add_case "mock" "fundamental-table" "0" "$mock_env" "stocks fundamental $live_symbol" "25.40"
|
||||||
add_case "mock" "valuation-json" "0" "$mock_env" "-o json stocks valuation $live_symbol" ""
|
add_case "mock" "fundamental-json" "0" "$mock_env" "-o json stocks fundamental $live_symbol" "" '.growth.revenue_growth_pct == 8.1 and .valuation.pe_trailing == 25.4 and .risk.debt_to_equity == 0.42'
|
||||||
add_case "mock" "risk-table" "0" "$mock_env" "stocks risk $live_symbol" ""
|
add_case "mock" "compare-json" "0" "$mock_env" "-o json stocks compare $compare_args" "" 'length > 0 and all(.[]; .growth.revenue_growth_pct == 8.1 and .valuation.pe_trailing == 25.4)'
|
||||||
add_case "mock" "risk-json" "0" "$mock_env" "-o json stocks risk $live_symbol" ""
|
add_case "mock" "profile-json" "0" "$mock_env" "-o json stocks profile $live_symbol" "" '.country == "Indonesia" and .employees == 27937'
|
||||||
add_case "mock" "fundamental-table" "0" "$mock_env" "stocks fundamental $live_symbol" ""
|
add_case "mock" "financials-table" "0" "$mock_env" "stocks financials $live_symbol" "400,000,000"
|
||||||
add_case "mock" "fundamental-json" "0" "$mock_env" "-o json stocks fundamental $live_symbol" ""
|
add_case "mock" "financials-json" "0" "$mock_env" "-o json stocks financials $live_symbol" "" '.income_statement.values.netIncome == 400000000 and .cash_flow.values.operatingCashFlow == 600000000'
|
||||||
add_case "mock" "compare-table" "0" "$mock_env" "stocks compare $compare_args" ""
|
add_case "mock" "earnings-json" "0" "$mock_env" "-o json stocks earnings $live_symbol" "" '.eps_last_year == 1200 and .forecast[0].eps_forecast == 1300 and .history[0].eps_actual == 1250'
|
||||||
add_case "mock" "compare-json" "0" "$mock_env" "-o json stocks compare $compare_args" ""
|
add_case "mock" "sentiment-json" "0" "$mock_env" "-o json stocks sentiment $live_symbol" "" '.statistics[0] == {"time_range":"1D","bullish":10,"bearish":2,"neutral":3}'
|
||||||
add_case "mock" "profile-table" "0" "$mock_env" "stocks profile $live_symbol" ""
|
add_case "mock" "insights-json" "0" "$mock_env" "-o json stocks insights $live_symbol" "" '.last_updated == "2026-03-26T04:14:57.9197955Z"'
|
||||||
add_case "mock" "profile-json" "0" "$mock_env" "-o json stocks profile $live_symbol" ""
|
add_case "mock" "news-json" "0" "$mock_env" "-o json $news_args" "" "map(.id) == ([\"news-1\"][:$NEWS_LIMIT])"
|
||||||
add_case "mock" "financials-table" "0" "$mock_env" "stocks financials $live_symbol" ""
|
add_case "mock" "screen-json" "0" "$mock_env" "-o json $screen_args" "" "map(.symbol) == ([\"BBRI.JK\", \"BBCA.JK\"][:$SCREEN_LIMIT])"
|
||||||
add_case "mock" "financials-json" "0" "$mock_env" "-o json stocks financials $live_symbol" ""
|
|
||||||
add_case "mock" "earnings-table" "0" "$mock_env" "stocks earnings $live_symbol" ""
|
|
||||||
add_case "mock" "earnings-json" "0" "$mock_env" "-o json stocks earnings $live_symbol" ""
|
|
||||||
add_case "mock" "sentiment-table" "0" "$mock_env" "stocks sentiment $live_symbol" ""
|
|
||||||
add_case "mock" "sentiment-json" "0" "$mock_env" "-o json stocks sentiment $live_symbol" ""
|
|
||||||
add_case "mock" "insights-table" "0" "$mock_env" "stocks insights $live_symbol" ""
|
|
||||||
add_case "mock" "insights-json" "0" "$mock_env" "-o json stocks insights $live_symbol" ""
|
|
||||||
add_case "mock" "news-table" "0" "$mock_env" "$news_args" ""
|
|
||||||
add_case "mock" "news-json" "0" "$mock_env" "-o json $news_args" ""
|
|
||||||
add_case "mock" "screen-table" "0" "$mock_env" "$screen_args" ""
|
|
||||||
add_case "mock" "screen-json" "0" "$mock_env" "-o json $screen_args" ""
|
|
||||||
|
|
||||||
add_case "cache" "quote-warm" "0" "$quote_zero_ttl_env" "stocks quote $live_symbol" ""
|
add_case "cache" "quote-warm" "0" "$quote_zero_ttl_env" "-o json stocks quote $live_symbol" ""
|
||||||
add_case "cache" "quote-offline" "0" "$quote_zero_ttl_env" "--offline stocks quote $live_symbol" ""
|
add_case "cache" "quote-offline" "0" "$quote_zero_ttl_env IDX_MOCK_ERROR=1" "-o json --offline stocks quote $live_symbol" ""
|
||||||
add_case "cache" "quote-stale-fallback" "0" "$quote_zero_ttl_env IDX_MOCK_ERROR=1" "stocks quote $live_symbol" "serving stale cache"
|
add_case "cache" "quote-stale-fallback" "0" "$quote_zero_ttl_env IDX_MOCK_ERROR=1" "-o json stocks quote $live_symbol" ""
|
||||||
add_case "cache" "technical-warm" "0" "$quote_zero_ttl_env" "stocks technical $live_symbol" ""
|
add_case "cache" "technical-warm" "0" "$quote_zero_ttl_env" "-o json stocks technical $live_symbol" ""
|
||||||
add_case "cache" "technical-offline" "0" "$quote_zero_ttl_env" "--offline stocks technical $live_symbol" ""
|
add_case "cache" "technical-offline" "0" "$quote_zero_ttl_env IDX_MOCK_ERROR=1" "-o json --offline stocks technical $live_symbol" ""
|
||||||
add_case "cache" "technical-stale-fallback" "0" "$quote_zero_ttl_env IDX_MOCK_ERROR=1" "stocks technical $live_symbol" "serving stale cache"
|
add_case "cache" "technical-stale-fallback" "0" "$quote_zero_ttl_env IDX_MOCK_ERROR=1" "-o json stocks technical $live_symbol" ""
|
||||||
add_case "cache" "profile-warm" "0" "$profile_zero_ttl_env" "stocks profile $live_symbol" ""
|
add_case "cache" "profile-warm" "0" "$profile_zero_ttl_env" "-o json stocks profile $live_symbol" ""
|
||||||
add_case "cache" "profile-offline" "0" "$profile_zero_ttl_env" "--offline stocks profile $live_symbol" ""
|
add_case "cache" "profile-offline" "0" "$profile_zero_ttl_env IDX_MOCK_ERROR=1" "-o json --offline stocks profile $live_symbol" ""
|
||||||
add_case "cache" "profile-stale-fallback" "0" "$profile_zero_ttl_env IDX_MOCK_ERROR=1" "stocks profile $live_symbol" "serving stale cache"
|
add_case "cache" "profile-stale-fallback" "0" "$profile_zero_ttl_env IDX_MOCK_ERROR=1" "-o json stocks profile $live_symbol" ""
|
||||||
|
|
||||||
add_case "routing" "yahoo-quote" "0" "$yahoo_mock_env" "stocks quote $live_symbol" ""
|
add_case "routing" "yahoo-quote" "0" "$yahoo_mock_env" "-o json --no-cache stocks quote $live_symbol" "" '.[0].price == 9875'
|
||||||
add_case "routing" "auto-history-fallback" "0" "$mock_env" "$history_args" ""
|
add_case "routing" "explicit-msn-history" "0" "$mock_env" "-o json --no-cache stocks history $live_symbol --period 3mo --history-provider msn" "" '.[0].close == 8000 and all(.[]; .volume == 0 and .open == .close and .high == .close and .low == .close)'
|
||||||
add_case "routing" "auto-technical-fallback" "0" "$mock_env" "stocks technical $live_symbol" ""
|
|
||||||
add_case "routing" "explicit-msn-history" "0" "$mock_env" "stocks history $live_symbol --period 3mo --history-provider msn" "History for"
|
|
||||||
|
|
||||||
add_case "errors" "invalid-provider-json" "1" "IDX_PROVIDER=bogus" "-o json version" "\"error\": true"
|
add_case "errors" "invalid-provider-json" "1" "IDX_PROVIDER=bogus" "-o json version" "" '.code == "CONFIGERROR"'
|
||||||
add_case "errors" "profile-provider-gate-json" "1" "IDX_PROVIDER=yahoo" "-o json stocks profile $live_symbol" "requires --provider msn"
|
add_case "errors" "profile-provider-gate-json" "1" "IDX_PROVIDER=yahoo" "-o json stocks profile $live_symbol" "" '.code == "UNSUPPORTED"'
|
||||||
add_case "errors" "invalid-screen-region-json" "1" "$mock_env" "-o json stocks screen --region eu" "invalid screener region"
|
add_case "errors" "invalid-screen-region-json" "1" "$mock_env" "-o json stocks screen --region eu" "" '.code == "INVALIDINPUT"'
|
||||||
add_case "errors" "offline-no-cache" "1" "IDX_USE_MOCK_PROVIDER=1" "--offline --no-cache stocks quote $live_symbol" "cannot combine --offline with --no-cache"
|
add_case "errors" "offline-no-cache" "1" "IDX_USE_MOCK_PROVIDER=1" "-o json --offline --no-cache stocks quote $live_symbol" "" '.code == "INVALIDINPUT"'
|
||||||
|
|
||||||
add_case "ownership" "releases-empty" "0" "" "ownership releases" "No ownership releases imported yet."
|
add_case "ownership" "releases-empty" "0" "" "-o json ownership releases" "" '. == []'
|
||||||
add_case "ownership" "fetch-bing-unsupported" "1" "" "ownership import --fetch-bing $live_symbol" "--fetch-bing import is not implemented yet"
|
add_case "ownership" "fetch-bing-unsupported" "1" "" "-o json ownership import --fetch-bing $live_symbol" "" '.code == "UNSUPPORTED"'
|
||||||
|
|
||||||
add_case "ownership-import" "discover-default-above1" "0" "" "ownership discover --limit 1" "supported"
|
add_case "ownership-import" "discover-default-above1" "0" "" "ownership discover --limit 1" "supported"
|
||||||
add_case "ownership-import" "import-supported-above1" "0" "" "ownership import --url $OWNERSHIP_ABOVE1_URL" "Imported "
|
add_case "ownership-import" "import-supported-above1" "0" "" "ownership import --url $OWNERSHIP_ABOVE1_URL" "Imported "
|
||||||
|
|
@ -402,11 +383,10 @@ build_binary() {
|
||||||
|
|
||||||
if (( BUILD )); then
|
if (( BUILD )); then
|
||||||
build_log="$LOG_DIR/build.log"
|
build_log="$LOG_DIR/build.log"
|
||||||
(
|
if ! (
|
||||||
cd "$ROOT_DIR" || exit 1
|
cd "$ROOT_DIR" || exit 1
|
||||||
cargo build --quiet --bin idx
|
cargo build --quiet --bin idx
|
||||||
) >"$build_log" 2>&1
|
) >"$build_log" 2>&1; then
|
||||||
if [[ $? -ne 0 ]]; then
|
|
||||||
echo "build failed; see $build_log" >&2
|
echo "build failed; see $build_log" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
@ -428,12 +408,10 @@ bootstrap_case() {
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
(
|
if ! (
|
||||||
cd "$ROOT_DIR" || exit 1
|
cd "$ROOT_DIR" || exit 1
|
||||||
env "${BASE_ENV[@]}" "$BIN_PATH" "$@"
|
env "${BASE_ENV[@]}" "$BIN_PATH" "$@"
|
||||||
) >"$log_file" 2>&1
|
) >"$log_file" 2>&1; then
|
||||||
|
|
||||||
if [[ $? -ne 0 ]]; then
|
|
||||||
echo "bootstrap failed for $label; see $log_file" >&2
|
echo "bootstrap failed for $label; see $log_file" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
@ -459,12 +437,10 @@ discover_ownership_import_url() {
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
(
|
if ! (
|
||||||
cd "$ROOT_DIR" || exit 1
|
cd "$ROOT_DIR" || exit 1
|
||||||
env "${BASE_ENV[@]}" "$BIN_PATH" -o json ownership discover --family "$family" --limit 1
|
env "${BASE_ENV[@]}" "$BIN_PATH" -o json ownership discover --family "$family" --limit 1
|
||||||
) >"$log_file" 2>&1
|
) >"$log_file" 2>&1; then
|
||||||
|
|
||||||
if [[ $? -ne 0 ]]; then
|
|
||||||
echo "bootstrap failed for ownership-discover-$family; see $log_file" >&2
|
echo "bootstrap failed for ownership-discover-$family; see $log_file" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
@ -518,13 +494,54 @@ command_display() {
|
||||||
printf ' %s %s' "$BIN_PATH" "$cmd_spec"
|
printf ' %s %s' "$BIN_PATH" "$cmd_spec"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
json_contract() {
|
||||||
|
if [[ "$2" -ne 0 ]]; then
|
||||||
|
printf '%s\n' '.error == true and (.code | type == "string") and (.message | type == "string")'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
*"stocks quote "*|*"stocks screen"*)
|
||||||
|
printf '%s\n' 'type == "array" and all(.[]; (.symbol | type == "string") and (.price | type == "number") and has("change_pct"))' ;;
|
||||||
|
*"stocks history "*)
|
||||||
|
printf '%s\n' 'type == "array" and all(.[]; (.date | type == "string") and (.close | type == "number") and (.volume | type == "number"))' ;;
|
||||||
|
*"stocks technical "*)
|
||||||
|
printf '%s\n' '(.symbol | type == "string") and (.current_price | type == "number") and has("sma200") and (.signals.overall | type == "string")' ;;
|
||||||
|
*"stocks growth "*)
|
||||||
|
printf '%s\n' 'has("revenue_growth_pct") and has("earnings_growth_pct") and (.overall_signal | type == "string")' ;;
|
||||||
|
*"stocks valuation "*)
|
||||||
|
printf '%s\n' 'has("pe_trailing") and has("roe_pct") and (.overall_signal | type == "string")' ;;
|
||||||
|
*"stocks risk "*)
|
||||||
|
printf '%s\n' 'has("debt_to_equity") and has("roa_pct") and (.overall_signal | type == "string")' ;;
|
||||||
|
*"stocks fundamental "*)
|
||||||
|
printf '%s\n' '(.symbol | type == "string") and (.growth.overall_signal | type == "string") and (.valuation.overall_signal | type == "string") and (.risk.overall_signal | type == "string")' ;;
|
||||||
|
*"stocks compare "*)
|
||||||
|
printf '%s\n' 'type == "array" and all(.[]; (.symbol | type == "string") and (.growth | type == "object") and (.valuation | type == "object") and (.risk | type == "object"))' ;;
|
||||||
|
*"stocks profile "*)
|
||||||
|
printf '%s\n' '(.symbol | type == "string") and has("long_name") and has("country") and (.officers | type == "array")' ;;
|
||||||
|
*"stocks financials "*)
|
||||||
|
printf '%s\n' '(.instrument.symbol | type == "string") and has("income_statement") and has("balance_sheet") and has("cash_flow")' ;;
|
||||||
|
*"stocks earnings "*)
|
||||||
|
printf '%s\n' '(.symbol | type == "string") and (.history | type == "array") and (.forecast | type == "array")' ;;
|
||||||
|
*"stocks sentiment "*)
|
||||||
|
printf '%s\n' '(.symbol | type == "string") and (.statistics | type == "array") and all(.statistics[]; (.bullish | type == "number") and (.bearish | type == "number") and (.neutral | type == "number"))' ;;
|
||||||
|
*"stocks insights "*)
|
||||||
|
printf '%s\n' '(.symbol | type == "string") and (.highlights | type == "array") and (.risks | type == "array")' ;;
|
||||||
|
*"stocks news "*)
|
||||||
|
printf '%s\n' 'type == "array" and all(.[]; (.symbol | type == "string") and (.title | type == "string") and (.url | type == "string"))' ;;
|
||||||
|
*)
|
||||||
|
printf '%s\n' 'type == "object" or type == "array"' ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
run_case_line() {
|
run_case_line() {
|
||||||
local line="$1"
|
local line="$1"
|
||||||
local group label expected_exit env_spec cmd_spec expect_spec
|
local group label expected_exit env_spec cmd_spec expect_spec json_expect
|
||||||
local safe_name log_file status
|
local safe_name log_file stdout_file stderr_file output_file status contract baseline_file
|
||||||
|
local failure=""
|
||||||
local -a env_parts cmd_parts
|
local -a env_parts cmd_parts
|
||||||
|
|
||||||
IFS="$SEP" read -r group label expected_exit env_spec cmd_spec expect_spec <<< "$line"
|
IFS="$SEP" read -r group label expected_exit env_spec cmd_spec expect_spec json_expect <<< "$line"
|
||||||
|
|
||||||
if ! should_run_group "$group"; then
|
if ! should_run_group "$group"; then
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -532,7 +549,9 @@ run_case_line() {
|
||||||
|
|
||||||
TOTAL_CASES=$((TOTAL_CASES + 1))
|
TOTAL_CASES=$((TOTAL_CASES + 1))
|
||||||
safe_name="$(sanitize_name "${group}-${label}")"
|
safe_name="$(sanitize_name "${group}-${label}")"
|
||||||
log_file="$(printf '%s/%03d_%s.log' "$LOG_DIR" "$TOTAL_CASES" "$safe_name")"
|
log_file="$(printf '%s/%03d_%s' "$LOG_DIR" "$TOTAL_CASES" "$safe_name")"
|
||||||
|
stdout_file="$log_file.stdout"
|
||||||
|
stderr_file="$log_file.stderr"
|
||||||
|
|
||||||
printf '[%02d] %-10s %s\n' "$TOTAL_CASES" "$group" "$label"
|
printf '[%02d] %-10s %s\n' "$TOTAL_CASES" "$group" "$label"
|
||||||
|
|
||||||
|
|
@ -556,25 +575,50 @@ run_case_line() {
|
||||||
(
|
(
|
||||||
cd "$ROOT_DIR" || exit 1
|
cd "$ROOT_DIR" || exit 1
|
||||||
env "${BASE_ENV[@]}" "${env_parts[@]}" "$BIN_PATH" "${cmd_parts[@]}"
|
env "${BASE_ENV[@]}" "${env_parts[@]}" "$BIN_PATH" "${cmd_parts[@]}"
|
||||||
) >"$log_file" 2>&1
|
) >"$stdout_file" 2>"$stderr_file"
|
||||||
status=$?
|
status=$?
|
||||||
|
|
||||||
if [[ "$status" -ne "$expected_exit" ]]; then
|
output_file="$stdout_file"
|
||||||
FAILED_CASES=$((FAILED_CASES + 1))
|
if [[ "$status" -ne 0 ]]; then
|
||||||
FAILURES+=("$group/$label (exit $status, expected $expected_exit) -> $log_file")
|
output_file="$stderr_file"
|
||||||
printf ' FAIL exit=%s expected=%s log=%s\n' "$status" "$expected_exit" "$log_file"
|
|
||||||
return 0
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "$expect_spec" ]] && ! grep -Fq -- "$expect_spec" "$log_file"; then
|
if [[ "$status" -ne "$expected_exit" ]]; then
|
||||||
|
failure="exit $status, expected $expected_exit"
|
||||||
|
elif [[ "$status" -ne 0 && -s "$stdout_file" ]]; then
|
||||||
|
failure="error command wrote to stdout"
|
||||||
|
elif [[ -n "$expect_spec" ]] && ! grep -Fq -- "$expect_spec" "$output_file"; then
|
||||||
|
failure="missing '$expect_spec' in expected output stream"
|
||||||
|
elif [[ " $cmd_spec " == *" -o json "* ]]; then
|
||||||
|
contract="$(json_contract "$cmd_spec" "$status")"
|
||||||
|
if ! jq -e -s "length == 1 and (.[0] | ($contract) and (${json_expect:-true}))" \
|
||||||
|
"$output_file" > /dev/null 2>"$log_file.validation"; then
|
||||||
|
failure="invalid JSON document or output contract"
|
||||||
|
fi
|
||||||
|
elif [[ "$label" == "completions-bash" ]] && ! bash -n "$stdout_file"; then
|
||||||
|
failure="invalid Bash completion script"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$failure" && "$group" == "cache" ]]; then
|
||||||
|
baseline_file="$LOG_DIR/cache-${label%%-*}-baseline.json"
|
||||||
|
if [[ "$label" == *"-warm" ]]; then
|
||||||
|
jq -S . "$stdout_file" >"$baseline_file"
|
||||||
|
elif ! jq -S . "$stdout_file" | cmp -s "$baseline_file" -; then
|
||||||
|
failure="offline/stale data differs from the warmed cache"
|
||||||
|
elif [[ ! -s "$stderr_file" ]]; then
|
||||||
|
failure="expired-cache diagnostic missing from stderr"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$failure" ]]; then
|
||||||
FAILED_CASES=$((FAILED_CASES + 1))
|
FAILED_CASES=$((FAILED_CASES + 1))
|
||||||
FAILURES+=("$group/$label (missing '$expect_spec') -> $log_file")
|
FAILURES+=("$group/$label ($failure) -> $log_file.{stdout,stderr,validation}")
|
||||||
printf ' FAIL missing=%s log=%s\n' "$expect_spec" "$log_file"
|
printf ' FAIL %s logs=%s.{stdout,stderr,validation}\n' "$failure" "$log_file"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PASSED_CASES=$((PASSED_CASES + 1))
|
PASSED_CASES=$((PASSED_CASES + 1))
|
||||||
printf ' PASS log=%s\n' "$log_file"
|
printf ' PASS logs=%s.{stdout,stderr}\n' "$log_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
|
|
@ -582,6 +626,10 @@ main() {
|
||||||
|
|
||||||
parse_args "$@"
|
parse_args "$@"
|
||||||
prepare_paths
|
prepare_paths
|
||||||
|
if (( DRY_RUN == 0 )) && ! command -v jq >/dev/null 2>&1; then
|
||||||
|
echo "jq is required for smoke output validation; run inside nix develop" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
printf 'mode: %s\n' "$MODE"
|
printf 'mode: %s\n' "$MODE"
|
||||||
printf 'workdir: %s\n' "$WORKDIR"
|
printf 'workdir: %s\n' "$WORKDIR"
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ cp "$repo_root/scripts/publish-ownership-snapshot.sh" "$publisher"
|
||||||
chmod +x "$publisher"
|
chmod +x "$publisher"
|
||||||
|
|
||||||
# --- fake gh ---------------------------------------------------------------
|
# --- fake gh ---------------------------------------------------------------
|
||||||
cat >"$bin_dir/gh" <<'FAKE_GH'
|
printf '#!%s\n' "$BASH" >"$bin_dir/gh"
|
||||||
#!/usr/bin/env bash
|
cat >>"$bin_dir/gh" <<'FAKE_GH'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
state="${FAKE_GH_STATE:?}"
|
state="${FAKE_GH_STATE:?}"
|
||||||
remote="$state/remote"
|
remote="$state/remote"
|
||||||
|
|
@ -144,8 +144,8 @@ FAKE_GH
|
||||||
chmod +x "$bin_dir/gh"
|
chmod +x "$bin_dir/gh"
|
||||||
|
|
||||||
# --- fake idx --------------------------------------------------------------
|
# --- fake idx --------------------------------------------------------------
|
||||||
cat >"$bin_dir/idx" <<'FAKE_IDX'
|
printf '#!%s\n' "$BASH" >"$bin_dir/idx"
|
||||||
#!/usr/bin/env bash
|
cat >>"$bin_dir/idx" <<'FAKE_IDX'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
if [[ "${1:-}" == "version" ]]; then
|
if [[ "${1:-}" == "version" ]]; then
|
||||||
exit 0
|
exit 0
|
||||||
|
|
@ -160,8 +160,8 @@ FAKE_IDX
|
||||||
chmod +x "$bin_dir/idx"
|
chmod +x "$bin_dir/idx"
|
||||||
|
|
||||||
# --- stub builder ----------------------------------------------------------
|
# --- stub builder ----------------------------------------------------------
|
||||||
cat >"$scripts_dir/build-latest-ownership-snapshot.sh" <<'STUB_BUILDER'
|
printf '#!%s\n' "$BASH" >"$scripts_dir/build-latest-ownership-snapshot.sh"
|
||||||
#!/usr/bin/env bash
|
cat >>"$scripts_dir/build-latest-ownership-snapshot.sh" <<'STUB_BUILDER'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
out=""
|
out=""
|
||||||
history="0"
|
history="0"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue