Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,12 @@ jobs:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

test-action:
name: "Test: Actions"
runs-on: ubuntu-latest
name: "Test: Actions (${{ matrix.os }})"
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]

steps:
- name: Harden Runner
Expand All @@ -88,9 +92,12 @@ jobs:
uses: ./
with: {}

- name: "Test: Print Output"
id: output
run: echo "${{ steps.test-action.outputs.path }}"
- name: "Test: Verify Installation"
shell: bash
run: |
elide --version
echo "Path: ${{ steps.test-action.outputs.path }}"
echo "Version: ${{ steps.test-action.outputs.version }}"

check-dist:
name: "Test: Dist"
Expand Down
98 changes: 98 additions & 0 deletions __tests__/install-apt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import * as exec from '@actions/exec'
import * as io from '@actions/io'
import * as core from '@actions/core'
import * as command from '../src/command'
import { installViaApt } from '../src/install-apt'
import buildOptions from '../src/options'

describe('install-apt', () => {
const execSpy = jest.spyOn(exec, 'exec')
const whichSpy = jest.spyOn(io, 'which')
const obtainVersionSpy = jest.spyOn(command, 'obtainVersion')

beforeEach(() => {
jest.clearAllMocks()

// suppress log output
jest.spyOn(core, 'info').mockImplementation(() => {})
jest.spyOn(core, 'debug').mockImplementation(() => {})

// default mocks: all exec calls succeed
execSpy.mockResolvedValue(0)
whichSpy.mockResolvedValue('/usr/bin/elide')
obtainVersionSpy.mockResolvedValue('1.0.0')
})

it('should run the correct apt commands for amd64', async () => {
const options = buildOptions({
os: 'linux',
arch: 'amd64',
version: 'latest'
})
const result = await installViaApt(options)

// GPG key download
expect(execSpy).toHaveBeenCalledWith('bash', [
'-c',
expect.stringContaining('keys.elide.dev/gpg.key')
])

// apt source with correct arch
expect(execSpy).toHaveBeenCalledWith(
'sudo',
['tee', '/etc/apt/sources.list.d/elide.list'],
expect.objectContaining({
input: expect.any(Buffer)
})
)

// apt-get update
expect(execSpy).toHaveBeenCalledWith('sudo', ['apt-get', 'update', '-qq'])

// apt-get install elide (no version pin for latest)
expect(execSpy).toHaveBeenCalledWith('sudo', [
'apt-get',
'install',
'-y',
'-qq',
'elide'
])

expect(result.elidePath).toBe('/usr/bin/elide')
expect(result.version.tag_name).toBe('1.0.0')
})

it('should map aarch64 to arm64 for apt', async () => {
const options = buildOptions({
os: 'linux',
arch: 'aarch64',
version: 'latest'
})
await installViaApt(options)

// Check that the tee call includes arm64
const teeCall = execSpy.mock.calls.find(
call => call[0] === 'sudo' && call[1]?.[0] === 'tee'
)
expect(teeCall).toBeDefined()
const input = teeCall![2] as { input: Buffer }
expect(input.input.toString()).toContain('arch=arm64')
})

it('should pin a specific version when requested', async () => {
const options = buildOptions({
os: 'linux',
arch: 'amd64',
version: '1.2.3'
})
await installViaApt(options)

expect(execSpy).toHaveBeenCalledWith('sudo', [
'apt-get',
'install',
'-y',
'-qq',
'elide=1.2.3'
])
})
})
76 changes: 76 additions & 0 deletions __tests__/install-script.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import * as exec from '@actions/exec'
import * as io from '@actions/io'
import * as core from '@actions/core'
import * as toolCache from '@actions/tool-cache'
import * as command from '../src/command'
import { installViaScript } from '../src/install-script'
import buildOptions from '../src/options'

describe('install-script', () => {
const execSpy = jest.spyOn(exec, 'exec')
const whichSpy = jest.spyOn(io, 'which')
const downloadToolSpy = jest.spyOn(toolCache, 'downloadTool')
const obtainVersionSpy = jest.spyOn(command, 'obtainVersion')
const addPathSpy = jest.spyOn(core, 'addPath')

beforeEach(() => {
jest.clearAllMocks()

// suppress log output
jest.spyOn(core, 'info').mockImplementation(() => {})
jest.spyOn(core, 'debug').mockImplementation(() => {})

// default mocks
downloadToolSpy.mockResolvedValue('/tmp/install.sh')
execSpy.mockResolvedValue(0)
whichSpy.mockResolvedValue('/usr/local/bin/elide')
obtainVersionSpy.mockResolvedValue('1.0.0')
})

it('should download and execute the install script', async () => {
const options = buildOptions({
os: 'darwin',
arch: 'aarch64',
version: 'latest'
})
const result = await installViaScript(options)

expect(downloadToolSpy).toHaveBeenCalledWith(
'https://dl.elide.dev/cli/install.sh'
)
expect(execSpy).toHaveBeenCalledWith('bash', ['/tmp/install.sh'])
expect(result.elidePath).toBe('/usr/local/bin/elide')
expect(result.version.tag_name).toBe('1.0.0')
})

it('should pass --version when a specific version is requested', async () => {
const options = buildOptions({
os: 'linux',
arch: 'amd64',
version: '1.2.3'
})
await installViaScript(options)

expect(execSpy).toHaveBeenCalledWith('bash', [
'/tmp/install.sh',
'--version',
'1.2.3'
])
})

it('should fall back to ~/.elide/bin if which fails initially', async () => {
whichSpy
.mockRejectedValueOnce(new Error('not found'))
.mockResolvedValueOnce('/home/runner/.elide/bin/elide')

const options = buildOptions({
os: 'linux',
arch: 'amd64',
version: 'latest'
})
const result = await installViaScript(options)

expect(addPathSpy).toHaveBeenCalled()
expect(result.elidePath).toBe('/home/runner/.elide/bin/elide')
})
})
32 changes: 24 additions & 8 deletions __tests__/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import * as io from '@actions/io'
import * as core from '@actions/core'
import * as platform from '../src/platform'
import * as command from '../src/command'
import * as installScript from '../src/install-script'
import * as main from '../src/main'
import buildOptions, { OptionName } from '../src/options'
import { ElideArch, ElideOS } from '../src/releases'
Expand All @@ -9,6 +12,15 @@ import { resolveExistingBinary } from '../src/main'
// set timeout to 3 minutes to account for downloads
jest.setTimeout(3 * 60 * 1000)

// Mock platform-specific installers. The apt and script installers have
// their own dedicated test suites; here we just need the dispatch to
// succeed without re-running real installations.
jest.spyOn(platform, 'isDebianLike').mockResolvedValue(false)
const scriptSpy = jest.spyOn(installScript, 'installViaScript')
const prewarmSpy = jest.spyOn(command, 'prewarm')
const infoSpy = jest.spyOn(command, 'info')
const obtainVersionSpy = jest.spyOn(command, 'obtainVersion')

// Mock the GitHub Actions core libs
const getInput = jest.spyOn(core, 'getInput')
const setFailed = jest.spyOn(core, 'setFailed')
Expand Down Expand Up @@ -40,13 +52,23 @@ const action = jest.spyOn(main, 'run')
describe('action', () => {
beforeEach(() => {
jest.clearAllMocks()
// Re-apply mocks cleared by clearAllMocks
jest.spyOn(platform, 'isDebianLike').mockResolvedValue(false)
scriptSpy.mockResolvedValue({
version: { tag_name: '1.0.0', userProvided: false },
elidePath: '/mock/bin/elide',
elideHome: '/mock',
elideBin: '/mock/bin'
})
prewarmSpy.mockResolvedValue(undefined)
infoSpy.mockResolvedValue(undefined)
obtainVersionSpy.mockResolvedValue('1.0.0')
})

it('reads option inputs', async () => {
setupMocks()
await main.run()
expect(action).toHaveReturned()
expect(action).not.toThrow()
expect(setFailed).not.toHaveBeenCalled()
expect(getInput).toHaveBeenCalledWith(OptionName.VERSION)
expect(getInput).toHaveBeenCalledWith(OptionName.OS)
Expand All @@ -67,7 +89,6 @@ describe('action', () => {

await main.run()
expect(action).toHaveReturned()
expect(action).not.toThrow()
expect(setFailed).not.toHaveBeenCalled()
expect(setOutput).toHaveBeenCalledWith(
ActionOutputName.PATH,
Expand All @@ -84,10 +105,7 @@ describe('action', () => {
info.mockImplementationOnce(() => {
throw new Error('oh noes')
})
const runner = async () => {
await main.run()
}
expect(runner).not.toThrow()
await main.run()
expect(setFailed).toHaveBeenCalled()
})

Expand Down Expand Up @@ -144,7 +162,6 @@ describe('action', () => {
force: true
})
expect(action).toHaveReturned()
expect(action).not.toThrow()
expect(setFailed).not.toHaveBeenCalled()
expect(setOutput).toHaveBeenCalledWith(
ActionOutputName.PATH,
Expand All @@ -163,7 +180,6 @@ describe('action', () => {
version: '1.0.0-alpha9'
})
expect(action).toHaveReturned()
expect(action).not.toThrow()
expect(setFailed).not.toHaveBeenCalled()
expect(setOutput).toHaveBeenCalledWith(
ActionOutputName.PATH,
Expand Down
23 changes: 23 additions & 0 deletions __tests__/platform.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const mockAccess = jest.fn()
jest.mock('node:fs/promises', () => ({
access: mockAccess
}))

import { isDebianLike } from '../src/platform'

describe('platform detection', () => {
beforeEach(() => {
mockAccess.mockReset()
})

it('should return true when /etc/debian_version exists', async () => {
mockAccess.mockResolvedValueOnce(undefined)
expect(await isDebianLike()).toBe(true)
expect(mockAccess).toHaveBeenCalledWith('/etc/debian_version')
})

it('should return false when /etc/debian_version does not exist', async () => {
mockAccess.mockRejectedValueOnce(new Error('ENOENT: no such file'))
expect(await isDebianLike()).toBe(false)
})
})
Loading
Loading