Skip to content

Commit

Permalink
Add unittests written in typescript
Browse files Browse the repository at this point in the history
Currently all of the tests are written as a github workflow. This
makes them very inconvenient to run as one would have to be triggering
the online CI every times they make a change to the code. On top of
this the tests can only verify the final product of the action, if
changes are made somewhere deep in the source code and they break
something it will not be immediately clear what is broken.

For this reason I've written tests in plain typescript that are much
easier to be run but still check all the platforms (linux, windows,
and macos) on x86_64 and arm64. These tests are covering a big part of
the source code, mostly the logic bits in addition to mocking network
responses for the code that contacts github.com or dlang.org to
determine the versions that needs to be installed. This should make
future work on the action way easier.

This doesn't make the old tests obsolete as some parts of the code are
not easily tested, like fetching release archives and properly
extracting them.

Add an action that will run the unittests on each system (linux,
macos, windows) to assure that any person can run the tests on their
system without failures.

Signed-off-by: Andrei Horodniceanu <[email protected]>
  • Loading branch information
the-horo committed Jul 11, 2024
1 parent 820e3c0 commit 54d541a
Show file tree
Hide file tree
Showing 14 changed files with 8,361 additions and 202 deletions.
32 changes: 32 additions & 0 deletions .github/workflows/unittests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Run the typescript unittests
on:
push:
branches:
- "v*"
pull_request:
branches:
- "*"
defaults:
run:
shell: bash

jobs:
run-typescript-unittests:
name: Run all the typescript unittests
strategy:
matrix:
# This could be run on only one machine but run on all of them
# to make sure that other developers can run the tests on
# their system.
os: [ macos-latest, ubuntu-latest, windows-latest ]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Run `npm test`
run: |
set -euxo pipefail
npm ci
npm test
161 changes: 161 additions & 0 deletions __tests__/action_input.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import * as main from '../src/main'
import * as core from '@actions/core'
import * as d from '../src/d'

describe('Testing compiler when...', function(){
let originalArch: string

beforeAll(function(){
originalArch = process.arch;
jest.spyOn(core, 'getInput').mockReturnValue('')
});

test('it is unspecified', () => {
Object.defineProperty(process, 'arch', { value: 'x64' })
expect(main.getActionInputs()).toStrictEqual({
d_compiler: 'dmd-latest', gh_token: '', dub_version: '', gdmd_sha: '',})

Object.defineProperty(process, 'arch', { value: 'arm64' })
expect(main.getActionInputs()).toStrictEqual({
d_compiler: 'ldc-latest', gh_token: '', dub_version: '', gdmd_sha: '',})
})

function mockCompiler(compiler: string) {
jest.spyOn(core, 'getInput').mockImplementation((key) => {
if (key == "compiler")
return compiler
return ""
})
}

test('it is specified', () => {
Object.defineProperty(process, 'arch', { value: 'x64' })
mockCompiler('ldc')
expect(main.getActionInputs()).toStrictEqual({
d_compiler: 'ldc', gh_token: '', dub_version: '', gdmd_sha: '',})

Object.defineProperty(process, 'arch', { value: 'x64' })
mockCompiler('invalid text')
expect(main.getActionInputs()).toStrictEqual({
d_compiler: 'invalid text', gh_token: '', dub_version: '', gdmd_sha: '',})
})

test('it is dmd on arm64', () => {
Object.defineProperty(process, 'arch', { value: 'arm64' })
mockCompiler('dmd-beta')
expect(main.getActionInputs).toThrow('dmd')
})

afterAll(function(){
Object.defineProperty(process, 'arch', { value: originalArch });
});
});

test('All action inputs', () => {
jest.spyOn(core, 'getInput').mockImplementation((key) => {
switch (key) {
case "compiler":
return "x"
case "dub":
return "y"
case "gh_token":
return "z"
case "gdmd_sha":
return "t"
default:
throw new Error(`Unknown key '${key}'`)
}
})

expect(main.getActionInputs()).toStrictEqual({
d_compiler: 'x', dub_version: 'y', gh_token: 'z', gdmd_sha: 't'
})
})

describe('Action messages', () => {
let nopTool = { makeAvailable: jest.fn() }
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {})

beforeAll(() => {
// Bypass type safety
jest.spyOn(d.DMD, 'initialize').mockResolvedValue(<any>nopTool)
jest.spyOn(d.LDC, 'initialize').mockResolvedValue(<any>nopTool)
jest.spyOn(d.GDC, 'initialize').mockResolvedValue(<any>nopTool)
jest.spyOn(d.GDMD, 'initialize').mockResolvedValue(<any>nopTool)
jest.spyOn(d.Dub, 'initialize').mockResolvedValue(<any>nopTool)
// Silence the failures
jest.spyOn(core, 'setFailed').mockImplementation(() => {})
})
beforeEach(() => {
nopTool.makeAvailable.mockClear()
consoleSpy.mockClear()
})

function mockInputs(compiler: string, dub: string = '') {
jest.spyOn(core, 'getInput').mockImplementation((key) => {
if (key == "compiler")
return compiler
else if (key == 'dub')
return dub
return ''
})
}

test('Specifying both compiler and dub', async () => {
let compString = 'dmd-2.110.0-beta.1'
let dubString = 'dub-secret-version'
mockInputs(compString, dubString)
await main.run()

expect(nopTool.makeAvailable).toHaveBeenCalledTimes(2)
expect(consoleSpy.mock.calls.length).toBe(2)
expect(consoleSpy.mock.calls[0][0]).toMatch(compString)
expect(consoleSpy.mock.calls[0][0]).toMatch(dubString)
expect(consoleSpy.mock.calls[1][0]).toMatch('Done')
})

test('Specifying only the compiler', async () => {
let compString = 'dmd-2.110.0-beta.1'
mockInputs(compString)
await main.run()

expect(nopTool.makeAvailable).toHaveBeenCalledTimes(1)
expect(consoleSpy.mock.calls.length).toBe(2)
expect(consoleSpy.mock.calls[0][0]).toMatch(compString)
expect(consoleSpy.mock.calls[0][0]).not.toMatch('dub')
expect(consoleSpy.mock.calls[1][0]).toMatch('Done')
})

test('Specifying an invalid compiler', async () => {
let compString = 'this-is-not-a-real-compiler'
mockInputs(compString)
await main.run()

expect(nopTool.makeAvailable).toHaveBeenCalledTimes(0)
expect(consoleSpy).toHaveBeenCalledTimes(1)
expect(consoleSpy.mock.calls[0][0]).toMatch(compString)
})

test('Specifying a valid compiler', async () => {
// This is only for coverage's sake
for (var comp of [ 'dmd', 'ldc', 'gdc', 'gdmd' ]) {
mockInputs(comp)
await main.run()
expect(nopTool.makeAvailable).toHaveBeenCalledTimes(1)
expect(consoleSpy).toHaveBeenCalledTimes(2)
nopTool.makeAvailable.mockClear()
consoleSpy.mockClear()
}
})

test('Errors are caught and displayed', async () => {
let msg = 'dmd-secret-recipe'
jest.spyOn(d.DMD, 'initialize').mockImplementation(async () => {
throw Error(msg)
})
mockInputs('dmd')
await main.run()
expect(consoleSpy).toHaveBeenCalledTimes(1)
expect(consoleSpy.mock.calls[0][0]).toMatch(msg)
})
})
124 changes: 124 additions & 0 deletions __tests__/d-compiler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Compiler } from '../src/d'
import fs from 'fs'

describe('Test Compiler class', () => {
const logSpy = jest.spyOn(console, 'log').mockReturnValue(undefined)
jest.spyOn(process.stdout, 'write').mockReturnValue(true)

let originalEnv = process.env
afterEach(() => process.env = originalEnv)

beforeEach(() => logSpy.mockClear())

const bin = '/relative/path/to/bin'
const libs = [ '/first', '/second' ]
const name = 'compiler_name'
let c = new Compiler('url', undefined, name, 'ver', bin, libs)
const root = '/root/folder'

// The values are computed when d.ts is imported so
// they will have the values of the host system, even
// if process.platform is modified in the tests.
const sep = process.platform == 'win32' ? '\\' : '/'
const extension = process.platform == 'win32' ? '.exe' : ''

test('Test setting PATH', () => {
process.env['PATH']='/bin'
c.addBinPath(root)
expect(process.env['PATH']).toBe(root + bin + ':/bin')
expect(logSpy).toHaveBeenCalledTimes(1)
})

test('Test setting LD_LIBRARY_PATH on UNIX', () => {
for (let platform of [ 'linux', 'freebsd', 'darwin' ]) {
Object.defineProperty(process, 'platform', { value: platform })

process.env['LD_LIBRARY_PATH']=''
jest.spyOn(fs, 'existsSync').mockReturnValue(true)

c.addLibPaths(root)
expect(process.env['LD_LIBRARY_PATH']).toBe(
root + libs[1] + ":" + root + libs[0])
expect(logSpy).toHaveBeenCalledTimes(2)
expect(logSpy.mock.calls[0][0]).toMatch(root + libs[0])
expect(logSpy.mock.calls[1][0]).toMatch(root + libs[1])
logSpy.mockClear()

process.env['LD_LIBRARY_PATH']=''
jest.spyOn(fs, 'existsSync')
.mockReturnValueOnce(false)
.mockReturnValueOnce(true)

c.addLibPaths(root)
expect(process.env['LD_LIBRARY_PATH']).toBe(root + libs[1])
expect(logSpy).toHaveBeenCalledTimes(1)
expect(logSpy.mock.calls[0][0]).toMatch(root + libs[1])
logSpy.mockClear()
}
})

test('Test setting PATH for libraries on windows', () => {
Object.defineProperty(process, 'platform', { value: 'win32' })

process.env['PATH']='\\bin'
jest.spyOn(fs, 'existsSync').mockReturnValue(true)

c.addLibPaths(root)
expect(process.env['PATH']).toBe(
root + libs[1] + ":" + root + libs[0] + ':\\bin')
expect(logSpy).toHaveBeenCalledTimes(2)
expect(logSpy.mock.calls[0][0]).toMatch(root + libs[0])
expect(logSpy.mock.calls[1][0]).toMatch(root + libs[1])
logSpy.mockClear()

process.env['PATH']='\\dir'
jest.spyOn(fs, 'existsSync')
.mockReturnValueOnce(true)
.mockReturnValueOnce(false)

c.addLibPaths(root)
expect(process.env['PATH']).toBe(root + libs[0] + ':\\dir')
expect(logSpy).toHaveBeenCalledTimes(1)
expect(logSpy.mock.calls[0][0]).toMatch(root + libs[0])
logSpy.mockClear()
})

test('Test makeAvailable', async () => {
jest.spyOn(c, 'getCached').mockResolvedValue(root)

for (const platform of [ 'linux', 'darwin', 'freebsd' ]) {
Object.defineProperty(process, 'platform', { value: platform })
jest.spyOn(fs, 'existsSync').mockReturnValue(true).
mockReturnValueOnce(false)

process.env['PATH'] = '/bin'
process.env['LD_LIBRARY_PATH'] = ''
await c.makeAvailable()

expect(process.env['PATH']).toBe(root + bin + ':/bin')
expect(process.env['LD_LIBRARY_PATH']).toBe(root + libs[1])
expect(process.env['DC']).toBe(`${root}${bin}${sep}${name}${extension}`)
}


Object.defineProperty(process, 'platform', { value: 'win32' })
jest.spyOn(fs, 'existsSync').mockReturnValue(true)

process.env['PATH'] = '\\bin'
//logSpy.mockRestore()
await c.makeAvailable()

const expPath = `${root}${libs[1]}:` + `${root}${libs[0]}:` + `${root}${bin}:` +
'\\bin'
expect(process.env['PATH']).toBe(expPath)
expect(process.env['DC']).toBe(`${root}${bin}${sep}${name}${extension}`)
})

test('DC gets set to the absolute path of the compiler', () => {
for (const platform of [ 'linux', 'win32', 'darwin', 'freebsd' ]) {
Object.defineProperty(process, 'platform', { value: platform })
c.setDC(root)
expect(process.env['DC']).toBe(root + bin + sep + name + extension)
}
})
})
Loading

0 comments on commit 54d541a

Please sign in to comment.