diff --git a/README.md b/README.md index d710b427..58dfebb9 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,10 @@ If the proposal is _pending_ and does not yet have a number, use a letter. The p - `package.json` specifies what kind of proposal it is in a `agoricProposal` field. If it's a "Software Upgrade Proposal" it also includes additional parameters. - `use.sh` is the script that will be run in the USE stage of the build - `test.sh` is the script that will be _included_ in the TEST stage of the build, and run in CI +- `setup-test.sh` is an optional script which can be used to run setup steps _inside_ the container. It runs _before_ the chain starts. +- `teardown-test.sh` is an optional script which can be used to run teardown steps _inside_ the container. It runs _after_ the chain stops. +- `before-test-run.sh` is an optional script which can be used to run setup steps on the _host_ (like starting a follower). It runs _before_ the container launches. It needs to be placed in the **host** folder. +- `after-test-run.sh` is an optional script which can be used to run teardown steps on the _host_ (like stopping a follower). It runs _after_ the container exits. It needs to be placed in the **host** folder. ## Development diff --git a/packages/synthetic-chain/package.json b/packages/synthetic-chain/package.json index 9e740102..de317d4f 100644 --- a/packages/synthetic-chain/package.json +++ b/packages/synthetic-chain/package.json @@ -25,13 +25,15 @@ "chalk": "^5.4.1", "cosmjs-types": "^0.9.0", "execa": "^9.5.2", - "glob": "^11.0.1" + "glob": "^11.0.1", + "tmp": "0.2.3" }, "devDependencies": { "@agoric/cosmic-proto": "0.5.0-u18.5", "@types/better-sqlite3": "^7.6.12", "@types/glob": "^8.1.0", "@types/node": "^18.19.50", + "@types/tmp": "0.2.6", "ava": "^6.2.0", "ts-blank-space": "^0.5.1", "tsup": "^8.3.6", diff --git a/packages/synthetic-chain/public/upgrade-test-scripts/run_test.sh b/packages/synthetic-chain/public/upgrade-test-scripts/run_test.sh index b540fccd..78832cd3 100755 --- a/packages/synthetic-chain/public/upgrade-test-scripts/run_test.sh +++ b/packages/synthetic-chain/public/upgrade-test-scripts/run_test.sh @@ -28,10 +28,10 @@ source ./env_setup.sh cd /usr/src/proposals/"$PROPOSAL/" || fail "Proposal $PROPOSAL does not exist" -if test -f prepare-test.sh +if test -f setup-test.sh then - echo "[$PROPOSAL] Running prepare-test.sh" - ./prepare-test.sh + echo "[$PROPOSAL] Running setup-test.sh" + ./setup-test.sh fi echo "[$PROPOSAL] Starting agd" @@ -44,3 +44,9 @@ echo "[$PROPOSAL] Running test.sh." echo "[$PROPOSAL] Testing completed." killAgd + +if test -f teardown-test.sh +then + echo "[$PROPOSAL] Running teardown-test.sh" + ./teardown-test.sh +fi diff --git a/packages/synthetic-chain/src/cli/cli.ts b/packages/synthetic-chain/src/cli/cli.ts index f8cf983c..fa38c33c 100755 --- a/packages/synthetic-chain/src/cli/cli.ts +++ b/packages/synthetic-chain/src/cli/cli.ts @@ -117,7 +117,7 @@ switch (cmd) { console.log(chalk.cyan.bold(`Testing ${proposal.proposalName}`)); const image = imageNameForProposal(proposal, 'test'); bakeTarget(image.target, values.dry); - runTestImage(proposal); + runTestImage({ proposal }); // delete the image to reclaim disk space. The next build // will use the build cache. execSync('docker system df', { stdio: 'inherit' }); diff --git a/packages/synthetic-chain/src/cli/dockerfileGen.ts b/packages/synthetic-chain/src/cli/dockerfileGen.ts index e5ac9e77..52517f94 100755 --- a/packages/synthetic-chain/src/cli/dockerfileGen.ts +++ b/packages/synthetic-chain/src/cli/dockerfileGen.ts @@ -32,7 +32,12 @@ ENV UPGRADE_TO=${to} RUN echo '. /usr/src/upgrade-test-scripts/env_setup.sh' >> ~/.bashrc # copy scripts -COPY --link --chmod=755 ./upgrade-test-scripts/env_setup.sh ./upgrade-test-scripts/run_prepare_zero.sh /usr/src/upgrade-test-scripts/ +${createCopyCommand( + [], + './upgrade-test-scripts/env_setup.sh', + './upgrade-test-scripts/run_prepare_zero.sh', + '/usr/src/upgrade-test-scripts/', +)} SHELL ["/bin/bash", "-c"] # this is the only layer that starts ag0 RUN /usr/src/upgrade-test-scripts/run_prepare_zero.sh @@ -75,8 +80,18 @@ ENV \ UPGRADE_INFO=${JSON.stringify(encodeUpgradeInfo(upgradeInfo))} \ SKIP_PROPOSAL_VALIDATION=${skipProposalValidation} -COPY --exclude=test --exclude=test.sh --link --chmod=755 ./proposals/${path} /usr/src/proposals/${path} -COPY --link --chmod=755 ./upgrade-test-scripts/env_setup.sh ./upgrade-test-scripts/run_prepare.sh ./upgrade-test-scripts/start_to_to.sh /usr/src/upgrade-test-scripts/ +${createCopyCommand( + ['host', 'node_modules', 'test', 'test.sh'], + `./proposals/${path}`, + `/usr/src/proposals/${path}`, +)} +${createCopyCommand( + [], + './upgrade-test-scripts/env_setup.sh', + './upgrade-test-scripts/run_prepare.sh', + './upgrade-test-scripts/start_to_to.sh', + '/usr/src/upgrade-test-scripts/', +)} WORKDIR /usr/src/upgrade-test-scripts SHELL ["/bin/bash", "-c"] RUN ./run_prepare.sh ${path} @@ -100,8 +115,19 @@ FROM ghcr.io/agoric/agoric-sdk:${sdkImageTag} as execute-${proposalName} WORKDIR /usr/src/upgrade-test-scripts # base is a fresh sdk image so set up the proposal and its dependencies -COPY --exclude=test --exclude=test.sh --link --chmod=755 ./proposals/${path} /usr/src/proposals/${path} -COPY --link --chmod=755 ./upgrade-test-scripts/env_setup.sh ./upgrade-test-scripts/run_execute.sh ./upgrade-test-scripts/start_to_to.sh ./upgrade-test-scripts/install_deps.sh /usr/src/upgrade-test-scripts/ +${createCopyCommand( + ['host', 'node_modules', 'test', 'test.sh'], + `./proposals/${path}`, + `/usr/src/proposals/${path}`, +)} +${createCopyCommand( + ['test.sh'], + './upgrade-test-scripts/env_setup.sh', + './upgrade-test-scripts/run_execute.sh', + './upgrade-test-scripts/start_to_to.sh', + './upgrade-test-scripts/install_deps.sh', + '/usr/src/upgrade-test-scripts/', +)} RUN --mount=type=cache,target=/root/.yarn ./install_deps.sh ${path} COPY --link --from=prepare-${proposalName} /root/.agoric /root/.agoric @@ -122,15 +148,27 @@ RUN ./run_execute.sh ${planName} # EVAL ${proposalName} FROM use-${lastProposal.proposalName} as eval-${proposalName} -COPY --exclude=test --exclude=test.sh --link --chmod=755 ./proposals/${path} /usr/src/proposals/${path} +${createCopyCommand( + ['host', 'node_modules', 'test', 'test.sh'], + `./proposals/${path}`, + `/usr/src/proposals/${path}`, +)} WORKDIR /usr/src/upgrade-test-scripts # First stage of this proposal so install its deps. -COPY --link ./upgrade-test-scripts/install_deps.sh /usr/src/upgrade-test-scripts/ +${createCopyCommand( + [], + './upgrade-test-scripts/install_deps.sh', + '/usr/src/upgrade-test-scripts/', +)} RUN --mount=type=cache,target=/root/.yarn ./install_deps.sh ${path} -COPY --link --chmod=755 ./upgrade-test-scripts/*eval* /usr/src/upgrade-test-scripts/ +${createCopyCommand( + [], + './upgrade-test-scripts/*eval*', + '/usr/src/upgrade-test-scripts/', +)} SHELL ["/bin/bash", "-c"] RUN ./run_eval.sh ${path} `; @@ -149,7 +187,12 @@ FROM ${previousStage}-${proposalName} as use-${proposalName} WORKDIR /usr/src/upgrade-test-scripts -COPY --link --chmod=755 ./upgrade-test-scripts/run_use.sh ./upgrade-test-scripts/start_agd.sh /usr/src/upgrade-test-scripts/ +${createCopyCommand( + [], + './upgrade-test-scripts/run_use.sh', + './upgrade-test-scripts/start_agd.sh', + '/usr/src/upgrade-test-scripts/', +)} SHELL ["/bin/bash", "-c"] RUN ./run_use.sh ${path} ENTRYPOINT ./start_agd.sh @@ -171,11 +214,19 @@ FROM use-${proposalName} as test-${proposalName} # Previous stages copied excluding test files (see COPY above). It would be good # to copy only missing files, but there may be none. Fortunately, copying extra # does not invalidate other images because nothing depends on this layer. -COPY --link --chmod=755 ./proposals/${path} /usr/src/proposals/${path} +${createCopyCommand( + ['host', 'node_modules'], + `./proposals/${path}`, + `/usr/src/proposals/${path}`, +)} WORKDIR /usr/src/upgrade-test-scripts -COPY --link --chmod=755 ./upgrade-test-scripts/run_test.sh /usr/src/upgrade-test-scripts/ +${createCopyCommand( + [], + './upgrade-test-scripts/run_test.sh', + '/usr/src/upgrade-test-scripts/', +)} SHELL ["/bin/bash", "-c"] ENTRYPOINT ./run_test.sh ${path} `; @@ -196,6 +247,18 @@ FROM ${useImage} as latest }, }; +export const createCopyCommand = ( + exclusionList: Array, + ...files: Array +) => + [ + 'COPY', + '--link', + '--chmod=755', + ...exclusionList.map(excluded => `--exclude=${excluded}`), + ...files, + ].join(' '); + export function writeBakefileProposals( allProposals: ProposalInfo[], platforms?: Platform[], diff --git a/packages/synthetic-chain/src/cli/run.ts b/packages/synthetic-chain/src/cli/run.ts index 6cd4fc78..76ab0739 100755 --- a/packages/synthetic-chain/src/cli/run.ts +++ b/packages/synthetic-chain/src/cli/run.ts @@ -1,7 +1,29 @@ -import { execSync } from 'node:child_process'; -import { realpathSync } from 'node:fs'; +import { spawnSync } from 'node:child_process'; +import { existsSync, realpathSync } from 'node:fs'; +import { resolve as resolvePath } from 'node:path'; +import { fileSync as createTempFile } from 'tmp'; import { ProposalInfo, imageNameForProposal } from './proposals.js'; +const createMessageFile = (proposal: ProposalInfo) => + createTempFile({ prefix: proposal.proposalName }); + +const executeHostScriptIfPresent = ( + extraEnv: typeof process.env, + proposal: ProposalInfo, + scriptName: string, +) => { + const scriptPath = `${resolvePath('.')}/proposals/${proposal.path}/host/${scriptName}`; + if (existsSync(scriptPath)) { + console.log( + `Running script ${scriptName} for proposal ${proposal.proposalName}`, + ); + spawnSync(scriptPath, { + env: { ...process.env, ...extraEnv }, + stdio: 'inherit', + }); + } +}; + /** * Used to propagate a SLOGFILE environment variable into Docker containers. * Any file identified by such a variable will be created if it does not already @@ -14,17 +36,67 @@ const propagateSlogfile = env => { const { SLOGFILE } = env; if (!SLOGFILE) return []; - execSync('touch "$SLOGFILE"'); - return ['-e', 'SLOGFILE', '-v', `"$SLOGFILE:${realpathSync(SLOGFILE)}"`]; + spawnSync('touch', [SLOGFILE]); + + return [ + '--env', + `SLOGFILE=${SLOGFILE}`, + '--volume', + `${SLOGFILE}:${realpathSync(SLOGFILE)}`, + ]; }; -export const runTestImage = (proposal: ProposalInfo) => { - console.log(`Running test image for proposal ${proposal.proposalName}`); - const { name } = imageNameForProposal(proposal, 'test'); - const slogOpts = propagateSlogfile(process.env); - // 'rm' to remove the container when it exits - const cmd = `docker run ${slogOpts.join(' ')} --rm ${name}`; - execSync(cmd, { stdio: 'inherit' }); +export const runTestImage = ({ + extraDockerArgs = [], + proposal, + removeContainerOnExit = true, +}: { + extraDockerArgs?: Array; + proposal: ProposalInfo; + removeContainerOnExit?: boolean; +}) => { + const { name: messageFilePath, removeCallback: removeTempFileCallback } = + createMessageFile(proposal); + + const containerFilePath = '/root/message-file-path'; + + try { + executeHostScriptIfPresent( + { + MESSAGE_FILE_PATH: messageFilePath, + }, + proposal, + 'before-test-run.sh', + ); + + console.log(`Running test image for proposal ${proposal.proposalName}`); + const { name } = imageNameForProposal(proposal, 'test'); + spawnSync( + 'docker', + [ + 'run', + '--env', + `MESSAGE_FILE_PATH=${containerFilePath}`, + '--mount', + `source=${messageFilePath},target=${containerFilePath},type=bind`, + ...(removeContainerOnExit ? ['--rm'] : []), + ...propagateSlogfile(process.env), + ...extraDockerArgs, + name, + ], + { stdio: 'inherit' }, + ); + + executeHostScriptIfPresent( + { + MESSAGE_FILE_PATH: messageFilePath, + }, + proposal, + 'after-test-run.sh', + ); + } finally { + removeTempFileCallback(); + } }; export const debugTestImage = (proposal: ProposalInfo) => { @@ -32,22 +104,33 @@ export const debugTestImage = (proposal: ProposalInfo) => { console.log( ` Starting chain of test image for proposal ${proposal.proposalName} - + To get an interactive shell in the container, use an IDE feature like "Attach Shell" or this command:' - + docker exec -ti $(docker ps -q -f ancestor=${name}) bash - + And within that shell: cd /usr/src/proposals/${proposal.path} && ./test.sh - + To edit files you can use terminal tools like vim, or mount the container in your IDE. In VS Code the command is: Dev Containers: Attach to Running Container... `, ); - - const slogOpts = propagateSlogfile(process.env); - // start the chain with ports mapped - const cmd = `docker run ${slogOpts.join(' ')} --publish 26657:26657 --publish 1317:1317 --publish 9090:9090 --interactive --tty --entrypoint /usr/src/upgrade-test-scripts/start_agd.sh ${name}`; - execSync(cmd, { stdio: 'inherit' }); + return runTestImage({ + extraDockerArgs: [ + '--entrypoint', + '/usr/src/upgrade-test-scripts/start_agd.sh', + '--interactive', + '--publish', + '1317:1317', + '--publish', + '9090:9090', + '--publish', + '26657:26657', + '--tty', + ], + proposal, + removeContainerOnExit: false, + }); }; diff --git a/yarn.lock b/yarn.lock index 1ce78388..54bdd74f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26,12 +26,14 @@ __metadata: "@types/better-sqlite3": "npm:^7.6.12" "@types/glob": "npm:^8.1.0" "@types/node": "npm:^18.19.50" + "@types/tmp": "npm:0.2.6" ava: "npm:^6.2.0" better-sqlite3: "npm:^11.8.1" chalk: "npm:^5.4.1" cosmjs-types: "npm:^0.9.0" execa: "npm:^9.5.2" glob: "npm:^11.0.1" + tmp: "npm:0.2.3" ts-blank-space: "npm:^0.5.1" tsup: "npm:^8.3.6" typescript: "npm:^5.7.3" @@ -626,11 +628,18 @@ __metadata: linkType: hard "@types/node@npm:^18.19.50": - version: 18.19.74 - resolution: "@types/node@npm:18.19.74" + version: 18.19.50 + resolution: "@types/node@npm:18.19.50" dependencies: undici-types: "npm:~5.26.4" - checksum: 10c0/365d9cc2af934965aa6a8471e24ae80add815c15dc094e42a320c57c1ea5416032f0b7ef6f23e32174c34811fbb8d89ea8eaa1396548610fbb8ba317b6e93fbf + checksum: 10c0/36e6bc9eb47213ce94a868dad9504465ad89fba6af9f7954e22bb27fb17a32ac495f263d0cf4fdaee74becd7b2629609a446ec8c2b59b7a07bd587567c8a4782 + languageName: node + linkType: hard + +"@types/tmp@npm:0.2.6": + version: 0.2.6 + resolution: "@types/tmp@npm:0.2.6" + checksum: 10c0/a11bfa2cd8eaa6c5d62f62a3569192d7a2c28efdc5c17af0b0551db85816b2afc8156f3ca15ac76f0b142ae1403f04f44279871424233a1f3390b2e5fc828cd0 languageName: node linkType: hard @@ -3326,6 +3335,13 @@ __metadata: languageName: node linkType: hard +"tmp@npm:0.2.3": + version: 0.2.3 + resolution: "tmp@npm:0.2.3" + checksum: 10c0/3e809d9c2f46817475b452725c2aaa5d11985cf18d32a7a970ff25b568438e2c076c2e8609224feef3b7923fa9749b74428e3e634f6b8e520c534eef2fd24125 + languageName: node + linkType: hard + "to-regex-range@npm:^5.0.1": version: 5.0.1 resolution: "to-regex-range@npm:5.0.1"