Skip to content

Commit

Permalink
integration test infrastructure (pkgxdev#315)
Browse files Browse the repository at this point in the history
* integration test infrastructure
* symlink tests
  • Loading branch information
mxcl committed Jan 18, 2023
1 parent 5bed6c1 commit da8b2e6
Show file tree
Hide file tree
Showing 19 changed files with 273 additions and 208 deletions.
10 changes: 6 additions & 4 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ jobs:

- run: sed -i.bak "s/^const version = .*$/const version = \"${{ steps.tea.outputs.version }}\"/" src/app.ts
- run: tea -XE deno task compile
- run: ./tea --version # verify compilation works
- run: tar cJf tea-${{ steps.tea.outputs.version }}+${{ matrix.platform.build-id }}.tar.xz ./tea
- uses: actions/upload-artifact@v3
with:
Expand Down Expand Up @@ -137,10 +138,11 @@ jobs:
git tag ${{ env.TAG }}
git push origin ${{ env.TAG }}
- uses: mikepenz/release-changelog-builder-action@v3
id: build_changelog
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# says `no changes` every time… 🤦🏼
# - uses: mikepenz/release-changelog-builder-action@v3
# id: build_changelog
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- uses: softprops/action-gh-release@v1
with:
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ jobs:
- macos-latest
- ubuntu-latest
steps:
#TODO get desired deno version as an output from a pre-step
- uses: actions/checkout@v3
- uses: teaxyz/setup@v0
- run: tea -XE deno task test
- uses: denoland/setup-deno@v1 # using ourself to install deno could compromise the tests
- run: deno task test

lint:
runs-on: ubuntu-latest
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ the creator of [`brew`].
 


# tea/cli 0.19.4
# tea/cli 0.19.5

```sh
$ node --eval 'console.log("Hello World!")'
Expand Down Expand Up @@ -392,14 +392,17 @@ supporting them! Check out the docs for the [pantry] to learn more.

## FAQ

### How do I update packages?
### How do I update?

```sh
$ tea --sync
# ^^ updates the pantries, and any packages in the virtual-environment

$ tea --sync +deno.land
# ^^ updates specific packages

$ sh <(curl tea.xyz) --sync
# ^^ updates `tea` as well
```

### How do I view what is stowed?
Expand Down
2 changes: 1 addition & 1 deletion deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"test": "deno test --allow-net --allow-read --allow-env --allow-run --allow-write --unstable",
"typecheck": "deno check --unstable ./src/app.ts",
"run": "deno run --unstable --allow-all ./src/app.ts",
"compile": "deno compile --allow-read --allow-write --allow-net --allow-run --allow-env --unstable --output tea src/app.ts",
"compile": "deno compile --allow-read --allow-write --allow-net --allow-run --allow-env --unstable --output $INIT_CWD/tea src/app.ts",
"install": "deno compile --allow-all --unstable --output $TEA_PREFIX/tea.xyz/v$VERSION/bin/tea src/app.ts && scripts/repair.ts tea.xyz"
},
// ignore all files since the current style deviates from deno's default style.
Expand Down
2 changes: 1 addition & 1 deletion src/app.err-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Path from "path"
async function suggestions(err: TeaError) {
switch (err.id) {
case 'not-found: pantry: package.yml': {
const suggestion = await usePantry().getClosestPackageSuggestion(err.ctx.project)
const suggestion = await usePantry().getClosestPackageSuggestion(err.ctx.project).swallow()
return suggestion
? `did you mean \`${logger.teal(suggestion)}\`? otherwise… see you on GitHub?`
: undefined
Expand Down
14 changes: 12 additions & 2 deletions src/hooks/useExec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { hydrate, resolve, install as base_install, link } from "prefab"
import { VirtualEnv } from "./useVirtualEnv.ts"
import { flatten } from "./useShellEnv.ts"
import { Logger } from "./useLogger.ts"
import { pkg as pkgutils } from "utils"
import { panic, pkg as pkgutils } from "utils"
import * as semver from "semver"
import Path from "path"

Expand All @@ -21,6 +21,7 @@ export default async function({ pkgs, inject, sync, ...opts }: Parameters) {
if (arg0) cmd[0] = arg0?.toString() // if we downloaded it then we need to replace args[0]
const clutch = pkgs.length > 0
const env: Record<string, string> = {}
let post_install = (_installs: Installation[]) => {}

if (inject) {
const {version, srcroot, file, ...vrtenv} = inject
Expand Down Expand Up @@ -60,12 +61,21 @@ export default async function({ pkgs, inject, sync, ...opts }: Parameters) {
const found = await which(arg0)
if (found) {
pkgs.push(found)
cmd[0] = found.shebang
post_install = (installs: Installation[]) => {
// attempt to become full path to avoid a potential fork bomb scenario
// though if that happened it would be a bug in us ofc.
const install = installs.find(x => x.pkg.project == found.project) ?? panic()
cmd[0] = ["bin", "sbin"].compact(x =>
install.path.join(x, found.shebang).isExecutableFile()
)[0]?.string ?? found.shebang
}
}
}

const installations = await install(pkgs, sync)

post_install(installations)

Object.assign(env, flatten(await useShellEnv({ installations })))

return { env, cmd, pkgs: installations }
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export type Args = {
}

export function useArgs(args: string[], arg0: string): [Args, Flags & ConvenienceFlags] {
if (flags) throw new Error("contract-violated");
if (flags) throw new Error("contract-violated")

// pre 0.19.0 this was how we sourced our (more limited) shell magic
if (args.length == 1 && args[0] == "-Eds") {
Expand Down
8 changes: 6 additions & 2 deletions src/hooks/useSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ import useLogger, { Logger } from "./useLogger.ts"
import * as semver from "semver"
import Path from "path"

async function find_git(): Promise<Path | undefined> {
async function find_git({tea_ok}: {tea_ok: boolean} = {tea_ok: false}): Promise<Path | undefined> {
for (const path_ of Deno.env.get('PATH')?.split(':') ?? []) {
const path = Path.root.join(path_, 'git')
if (path.string == '/usr/bin/git' && host().platform == 'darwin' && !await clt_installed()) {
// if the CLT or Xcode is installed then we can use the system git
// if neither is installed then git will actually immediately exit with an error
continue
}
if (!tea_ok && path.isSymlink() && path.readlink().basename() == "tea") {
// we cannot install git via ourselves before we have fetched the pantries
continue
}
if (path.isExecutableFile()) {
return Promise.resolve(path)
}
Expand Down Expand Up @@ -142,7 +146,7 @@ export const update = async () => {
case 'noop': {
logger.replace("syncing pantries…")

const git = await find_git()
const git = await find_git({tea_ok: true})
if (!git) return console.warn("cannot update pantry without git")
const pp: Promise<void>[] = []
for await (const cwd of ls()) {
Expand Down
2 changes: 1 addition & 1 deletion src/vendor/Path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ export default class Path {
return to
}

/// creates a symlink from `this` to `to`
/// creates symlink `to` pointing at `this`
ln(_: 's', {to}: { to: Path }): Path {
Deno.symlinkSync(this.string, to.string)
return to
Expand Down
116 changes: 116 additions & 0 deletions tests/integration.suite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { describe } from "deno/testing/bdd.ts"
import { assert } from "deno/testing/asserts.ts"
import SemVer from "semver"
import Path from "path"

interface This {
tea: Path
sandbox: Path
TEA_PREFIX: Path
run: (opts: RunOptions) => Promise<number> & Enhancements
}

interface RunOptions {
args: string[]
env?: Record<string, string>
throws?: boolean
}

interface Enhancements {
stdout(): Promise<string>
}

const existing_www_cache = Path.home().join(".tea/tea.xyz/var/www")

const suite = describe({
name: "integration tests",
async beforeEach(this: This) {
const v = new SemVer(Deno.env.get("VERSION") ?? "1.2.3")
const tmp = new Path(await Deno.makeTempDir({ prefix: "tea" }))
const cwd = new URL(import.meta.url).path().parent().parent().string
const bin = tmp.join(`opt/tea.xyz/v${v}/bin`).mkpath()

const proc = Deno.run({
cmd: [
"deno",
"compile",
"--quiet",
"--allow-read", // restricting reads would be nice but Deno.symlink requires read permission to ALL
"--allow-write", // restricting writes would be nice but Deno.symlink requires write permission to ALL
"--allow-net",
"--allow-run",
"--allow-env",
"--unstable",
"--output", bin.join("tea").string,
"src/app.ts"
], cwd
})

assert((await proc.status()).success)
proc.close()

this.tea = bin.join("tea")
assert(this.tea.isExecutableFile())

this.TEA_PREFIX = tmp.join("opt")
assert(this.TEA_PREFIX.isDirectory())

this.sandbox = tmp.join("box").mkdir()

const teafile = bin.join('tea')
const { sandbox, TEA_PREFIX } = this

if (existing_www_cache.isDirectory()) {
// we're not testing our ISP
const to = this.TEA_PREFIX.join("tea.xyz/var").mkpath().join("www")
existing_www_cache.ln('s', {to})
}

this.run = ({args, env, throws}: RunOptions) => {
const cmd = [teafile.string, ...args]

env ??= {}
for (const key of ['HOME', 'CI', 'RUNNER_DEBUG', 'GITHUB_ACTIONS']) {
const value = Deno.env.get(key)
if (value) env[key] = value
}
env['PATH'] = "/usr/bin:/bin" // these systems are full of junk
env['TEA_PREFIX'] = TEA_PREFIX.string

let stdout: "piped" | undefined

// we delay instantiating the proc so we can set `stdout` if the user calls that function
// so the contract is the user must call `stdout` within this event loop iteration
const p = Promise.resolve().then(async () => {
const proc = Deno.run({ cmd, cwd: sandbox.string, stdout, env, clearEnv: true})
try {
const status = await proc.status()
if ((throws === undefined || throws) && !status.success) {
if (stdout == 'piped') proc.stdout?.close()
throw status
}
if (stdout == 'piped') {
const out = await proc.output()
return new TextDecoder().decode(out)
} else {
return status.code
}
} finally {
proc.close()
}
}) as Promise<number> & Enhancements

p.stdout = () => {
stdout = "piped"
return p as unknown as Promise<string>
}

return p
}
},
afterEach() {
// this.TEA_PREFIX.parent().rm({ recursive: true })
},
})

export default suite
24 changes: 13 additions & 11 deletions tests/integration/cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { assert } from "deno/testing/asserts.ts"
import { sandbox } from '../utils.ts'
import { assert, assertEquals, assertMatch } from "deno/testing/asserts.ts"
import suite from "../integration.suite.ts"
import { it } from "deno/testing/bdd.ts"

Deno.test("usage", async () => {
const out = await sandbox(({ run }) => run({args: ["--help"]}).stdout())
it(suite, "tea --help", async function() {
const out = await this.run({args: ["--help"]}).stdout()
assert(out.split("\n").length > 0)
})

Deno.test("+zlib.net", async () => {
await sandbox(async tea => {
await tea.run({
args: ["--sync", "+zlib.net", "true"],
net: true
})
})
it(suite, "tea +zlib.net", async function() {
const code = await this.run({ args: ["--sync", "+zlib.net", "true"] })
assertEquals(code, 0)
})

it(suite, "tea --version", async function() {
const out = await this.run({ args: ["--version"] }).stdout()
assertMatch(out, /tea \d+\.\d+\.\d+/)
})
32 changes: 0 additions & 32 deletions tests/integration/tea-XX.test.ts

This file was deleted.

Loading

0 comments on commit da8b2e6

Please sign in to comment.