mirror of
https://github.com/php/pie-ext-binary-builder.git
synced 2026-03-24 07:32:12 +01:00
530 lines
20 KiB
JavaScript
530 lines
20 KiB
JavaScript
const exec = require('@actions/exec');
|
|
const fs = require('fs');
|
|
const github = require('@actions/github');
|
|
const action = require('../src/index');
|
|
const core = require('@actions/core');
|
|
|
|
jest.mock('@actions/core');
|
|
jest.mock('@actions/exec');
|
|
jest.mock('@actions/github', () => ({
|
|
getOctokit: jest.fn(),
|
|
context: {
|
|
repo: {
|
|
owner: 'the-owner',
|
|
repo: 'the-repo',
|
|
},
|
|
},
|
|
}));
|
|
jest.mock('fs', () => ({
|
|
...jest.requireActual('fs'),
|
|
existsSync: jest.fn(),
|
|
readFileSync: jest.fn(),
|
|
}));
|
|
|
|
describe('determinePhpVersionFromPhpConfig', () => {
|
|
test('php version can be determined from php-config', async () => {
|
|
exec.getExecOutput.mockResolvedValue({
|
|
stdout: "8.3.10-whatever\n",
|
|
exitCode: 0,
|
|
});
|
|
|
|
expect(await action.determinePhpVersionFromPhpConfig())
|
|
.toBe('8.3');
|
|
});
|
|
});
|
|
|
|
describe ('determineExtensionNameFromComposerJson', () => {
|
|
test('composer.json does not exist', async () => {
|
|
await expect(action.determineExtensionNameFromComposerJson())
|
|
.rejects
|
|
.toThrow('composer.json not found. This does not appear to be a PIE package.');
|
|
});
|
|
|
|
test('composer.json has type other than php-ext or php-ext-zend', async () => {
|
|
fs.existsSync.mockReturnValue(true);
|
|
|
|
// jq -r ".type" composer.json
|
|
exec.getExecOutput.mockResolvedValue({
|
|
stdout: "library\n",
|
|
exitCode: 0,
|
|
});
|
|
|
|
await expect(action.determineExtensionNameFromComposerJson())
|
|
.rejects
|
|
.toThrow('composer.json type must be "php-ext" or "php-ext-zend", but "library" was found.');
|
|
});
|
|
|
|
test('composer.json has valid php-ext.extension-name with ext- prefix defined', async () => {
|
|
fs.existsSync.mockReturnValue(true);
|
|
exec.getExecOutput.mockImplementation((command, args) => {
|
|
// jq -r ".type" composer.json
|
|
if (args.includes('.type')) {
|
|
return Promise.resolve({ stdout: "php-ext\n", exitCode: 0 });
|
|
}
|
|
|
|
// jq -r '."php-ext"."extension-name"' composer.json
|
|
if (args.includes('."php-ext"."extension-name"')) {
|
|
return Promise.resolve({ stdout: "ext-test_ext\n", exitCode: 0 });
|
|
}
|
|
|
|
return Promise.reject(new Error('Test did not define command: ' + command + ' with args: ' + args));
|
|
});
|
|
|
|
expect(await action.determineExtensionNameFromComposerJson())
|
|
.toBe('test_ext');
|
|
});
|
|
|
|
test('composer.json has valid php-ext.extension-name defined', async () => {
|
|
fs.existsSync.mockReturnValue(true);
|
|
exec.getExecOutput.mockImplementation((command, args) => {
|
|
// jq -r ".type" composer.json
|
|
if (args.includes('.type')) {
|
|
return Promise.resolve({ stdout: "php-ext\n", exitCode: 0 });
|
|
}
|
|
|
|
// jq -r '."php-ext"."extension-name"' composer.json
|
|
if (args.includes('."php-ext"."extension-name"')) {
|
|
return Promise.resolve({ stdout: "test_ext\n", exitCode: 0 });
|
|
}
|
|
|
|
return Promise.reject(new Error('Test did not define command: ' + command + ' with args: ' + args));
|
|
});
|
|
|
|
expect(await action.determineExtensionNameFromComposerJson())
|
|
.toBe('test_ext');
|
|
});
|
|
|
|
test('composer.json has invalid php-ext.extension-name defined', async () => {
|
|
fs.existsSync.mockReturnValue(true);
|
|
exec.getExecOutput.mockImplementation((command, args) => {
|
|
// jq -r ".type" composer.json
|
|
if (args.includes('.type')) {
|
|
return Promise.resolve({ stdout: "php-ext\n", exitCode: 0 });
|
|
}
|
|
|
|
// jq -r '."php-ext"."extension-name"' composer.json
|
|
if (args.includes('."php-ext"."extension-name"')) {
|
|
return Promise.resolve({ stdout: "invalid-ext-name\n", exitCode: 0 });
|
|
}
|
|
|
|
return Promise.reject(new Error('Test did not define command: ' + command + ' with args: ' + args));
|
|
});
|
|
|
|
await expect(action.determineExtensionNameFromComposerJson())
|
|
.rejects
|
|
.toThrow('Invalid extension name: "invalid-ext-name" - must be alphanumeric/underscores only.');
|
|
});
|
|
|
|
test('composer.json has no php-ext.extension-name defined, but package name is valid', async () => {
|
|
fs.existsSync.mockReturnValue(true);
|
|
exec.getExecOutput.mockImplementation((command, args) => {
|
|
// jq -r ".type" composer.json
|
|
if (args.includes('.type')) {
|
|
return Promise.resolve({ stdout: "php-ext\n", exitCode: 0 });
|
|
}
|
|
|
|
// jq -r '."php-ext"."extension-name"' composer.json
|
|
if (args.includes('."php-ext"."extension-name"')) {
|
|
return Promise.resolve({ stdout: "\n", exitCode: 0 });
|
|
}
|
|
|
|
// jq -r '.name' composer.json
|
|
if (args.includes('.name')) {
|
|
return Promise.resolve({ stdout: "foo/bar\n", exitCode: 0 });
|
|
}
|
|
|
|
return Promise.reject(new Error('Test did not define command: ' + command + ' with args: ' + args));
|
|
});
|
|
|
|
expect(await action.determineExtensionNameFromComposerJson())
|
|
.toBe('bar');
|
|
});
|
|
|
|
test('composer.json has no php-ext.extension-name or package name defined', async () => {
|
|
fs.existsSync.mockReturnValue(true);
|
|
exec.getExecOutput.mockImplementation((command, args) => {
|
|
// jq -r ".type" composer.json
|
|
if (args.includes('.type')) {
|
|
return Promise.resolve({ stdout: "php-ext\n", exitCode: 0 });
|
|
}
|
|
|
|
// jq -r '."php-ext"."extension-name"' composer.json
|
|
if (args.includes('."php-ext"."extension-name"')) {
|
|
return Promise.resolve({ stdout: "\n", exitCode: 0 });
|
|
}
|
|
|
|
// jq -r '.name' composer.json
|
|
if (args.includes('.name')) {
|
|
return Promise.resolve({ stdout: "\n", exitCode: 0 });
|
|
}
|
|
|
|
return Promise.reject(new Error('Test did not define command: ' + command + ' with args: ' + args));
|
|
});
|
|
|
|
await expect(action.determineExtensionNameFromComposerJson())
|
|
.rejects
|
|
.toThrow('Could not determine extension name: both .\"php-ext\".\"extension-name\" and .name are missing in composer.json');
|
|
});
|
|
|
|
test('composer.json has no php-ext.extension-name defined, and package name is invalid', async () => {
|
|
fs.existsSync.mockReturnValue(true);
|
|
exec.getExecOutput.mockImplementation((command, args) => {
|
|
// jq -r ".type" composer.json
|
|
if (args.includes('.type')) {
|
|
return Promise.resolve({ stdout: "php-ext\n", exitCode: 0 });
|
|
}
|
|
|
|
// jq -r '."php-ext"."extension-name"' composer.json
|
|
if (args.includes('."php-ext"."extension-name"')) {
|
|
return Promise.resolve({ stdout: "\n", exitCode: 0 });
|
|
}
|
|
|
|
// jq -r '.name' composer.json
|
|
if (args.includes('.name')) {
|
|
return Promise.resolve({ stdout: "foo/invalid-ext-name\n", exitCode: 0 });
|
|
}
|
|
|
|
return Promise.reject(new Error('Test did not define command: ' + command + ' with args: ' + args));
|
|
});
|
|
|
|
await expect(action.determineExtensionNameFromComposerJson())
|
|
.rejects
|
|
.toThrow('Invalid extension name: "invalid-ext-name" - must be alphanumeric/underscores only.');
|
|
});
|
|
});
|
|
|
|
describe('determineArchitecture', () => {
|
|
const originalArch = process.arch;
|
|
|
|
afterEach(() => {
|
|
Object.defineProperty(process, 'arch', {
|
|
value: originalArch,
|
|
configurable: true
|
|
});
|
|
});
|
|
|
|
test('x64', async () => {
|
|
Object.defineProperty(process, 'arch', { value: 'x64', configurable: true });
|
|
expect(await action.determineArchitecture()).toBe('x86_64');
|
|
});
|
|
|
|
test('arm64', async () => {
|
|
Object.defineProperty(process, 'arch', { value: 'arm64', configurable: true });
|
|
expect(await action.determineArchitecture()).toBe('arm64');
|
|
});
|
|
|
|
test('ia32', async () => {
|
|
Object.defineProperty(process, 'arch', { value: 'ia32', configurable: true });
|
|
expect(await action.determineArchitecture()).toBe('x86');
|
|
});
|
|
|
|
test('unsupported architecture', async () => {
|
|
Object.defineProperty(process, 'arch', { value: 'bloop', configurable: true });
|
|
await expect(action.determineArchitecture())
|
|
.rejects
|
|
.toThrow('Unsupported architecture: bloop');
|
|
});
|
|
});
|
|
|
|
describe('determineOperatingSystem', () => {
|
|
const originalPlatform = process.platform;
|
|
|
|
afterEach(() => {
|
|
Object.defineProperty(process, 'platform', {
|
|
value: originalPlatform,
|
|
configurable: true
|
|
});
|
|
});
|
|
|
|
test('linux', async () => {
|
|
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
|
|
expect(await action.determineOperatingSystem()).toBe('linux');
|
|
});
|
|
|
|
test('darwin', async () => {
|
|
Object.defineProperty(process, 'platform', { value: 'darwin', configurable: true });
|
|
expect(await action.determineOperatingSystem()).toBe('darwin');
|
|
});
|
|
|
|
test('win32', async () => {
|
|
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
await expect(action.determineOperatingSystem())
|
|
.rejects
|
|
.toThrow('Unsupported operating system: win32');
|
|
});
|
|
});
|
|
|
|
describe('determineLibcFlavour', () => {
|
|
const originalPlatform = process.platform;
|
|
|
|
beforeEach(() => {
|
|
exec.getExecOutput.mockReset();
|
|
});
|
|
|
|
afterEach(() => {
|
|
Object.defineProperty(process, 'platform', {
|
|
value: originalPlatform,
|
|
configurable: true
|
|
});
|
|
});
|
|
|
|
test('osx uses bsdlibc', async () => {
|
|
Object.defineProperty(process, 'platform', { value: 'darwin', configurable: true });
|
|
await expect(action.determineLibcFlavour()).resolves.toBe('bsdlibc');
|
|
});
|
|
|
|
test('musl detected', async () => {
|
|
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
|
|
exec.getExecOutput.mockResolvedValue({ stdout: 'musl libc (1.2.4)\n', exitCode: 0 });
|
|
await expect(action.determineLibcFlavour()).resolves.toBe('musl');
|
|
});
|
|
|
|
test('otherwise glibc', async () => {
|
|
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
|
|
exec.getExecOutput.mockResolvedValue({ stdout: 'ldd (GNU libc) 2.31\n', exitCode: 0 });
|
|
await expect(action.determineLibcFlavour()).resolves.toBe('glibc');
|
|
});
|
|
});
|
|
|
|
describe('determinePhpBinary', () => {
|
|
test('php binary is returned', async () => {
|
|
exec.getExecOutput.mockResolvedValue({
|
|
stdout: "/path/to/php\n",
|
|
exitCode: 0,
|
|
});
|
|
await expect(action.determinePhpBinary()).resolves.toBe('/path/to/php');
|
|
});
|
|
|
|
test('php binary being NONE returns just php', async () => {
|
|
exec.getExecOutput.mockResolvedValue({
|
|
stdout: "NONE\n",
|
|
exitCode: 0,
|
|
});
|
|
await expect(action.determinePhpBinary()).resolves.toBe('php');
|
|
});
|
|
});
|
|
|
|
describe('determinePhpDebugMode', () => {
|
|
test('debug mode', async () => {
|
|
exec.getExecOutput.mockResolvedValue({
|
|
stdout: "-debug\n",
|
|
exitCode: 0,
|
|
});
|
|
await expect(action.determinePhpDebugMode()).resolves.toBe('-debug');
|
|
});
|
|
test('non debug mode', async () => {
|
|
exec.getExecOutput.mockResolvedValue({
|
|
stdout: "\n",
|
|
exitCode: 0,
|
|
});
|
|
await expect(action.determinePhpDebugMode()).resolves.toBe('');
|
|
});
|
|
});
|
|
|
|
describe('determineZendThreadSafeMode', () => {
|
|
test('zts mode', async () => {
|
|
exec.getExecOutput.mockResolvedValue({
|
|
stdout: "-zts\n",
|
|
exitCode: 0,
|
|
});
|
|
await expect(action.determineZendThreadSafeMode()).resolves.toBe('-zts');
|
|
});
|
|
|
|
test('nts mode', async () => {
|
|
exec.getExecOutput.mockResolvedValue({
|
|
stdout: "\n",
|
|
exitCode: 0,
|
|
});
|
|
await expect(action.determineZendThreadSafeMode()).resolves.toBe('');
|
|
});
|
|
});
|
|
|
|
describe('buildExtension', () => {
|
|
test('builds the extension with configure params and default build path', async () => {
|
|
core.getInput.mockImplementation((name) => {
|
|
if (name === 'configure-flags') return '--enable-test --with-foo=/foo/bar';
|
|
if (name === 'build-path') return '.';
|
|
return '';
|
|
});
|
|
|
|
await action.buildExtension();
|
|
|
|
expect(exec.exec).toHaveBeenCalledWith('phpize', [], {});
|
|
expect(exec.exec).toHaveBeenCalledWith('./configure', ['--enable-test', '--with-foo=/foo/bar'], {});
|
|
expect(exec.exec).toHaveBeenCalledWith('make', [], {});
|
|
});
|
|
|
|
test('builds the extension with custom build path', async () => {
|
|
core.getInput.mockImplementation((name) => {
|
|
if (name === 'configure-flags') return '--enable-test';
|
|
if (name === 'build-path') return 'some/ext/path';
|
|
return '';
|
|
});
|
|
|
|
await action.buildExtension();
|
|
|
|
expect(exec.exec).toHaveBeenCalledWith('phpize', [], { cwd: 'some/ext/path' });
|
|
expect(exec.exec).toHaveBeenCalledWith('./configure', ['--enable-test'], { cwd: 'some/ext/path' });
|
|
expect(exec.exec).toHaveBeenCalledWith('make', [], { cwd: 'some/ext/path' });
|
|
});
|
|
});
|
|
|
|
describe('uploadReleaseAsset', () => {
|
|
let octokit;
|
|
|
|
beforeEach(() => {
|
|
octokit = {
|
|
rest: {
|
|
repos: {
|
|
listReleases: jest.fn(),
|
|
uploadReleaseAsset: jest.fn(),
|
|
},
|
|
},
|
|
};
|
|
github.getOctokit.mockReturnValue(octokit);
|
|
core.getInput.mockReturnValue('fake-token');
|
|
});
|
|
|
|
test('successfully uploads asset', async () => {
|
|
octokit.rest.repos.listReleases.mockResolvedValue({
|
|
data: [
|
|
{
|
|
id: 123,
|
|
tag_name: '1.0.0',
|
|
name: 'Release 1.0.0',
|
|
},
|
|
],
|
|
});
|
|
fs.readFileSync.mockReturnValue('release-asset-fake-data');
|
|
|
|
await action.uploadReleaseAsset('1.0.0', 'release-asset.zip');
|
|
|
|
expect(github.getOctokit).toHaveBeenCalledWith('fake-token');
|
|
expect(octokit.rest.repos.listReleases).toHaveBeenCalledWith({
|
|
owner: 'the-owner',
|
|
repo: 'the-repo',
|
|
});
|
|
expect(octokit.rest.repos.uploadReleaseAsset).toHaveBeenCalledWith({
|
|
owner: 'the-owner',
|
|
repo: 'the-repo',
|
|
release_id: 123,
|
|
name: 'release-asset.zip',
|
|
data: 'release-asset-fake-data',
|
|
});
|
|
});
|
|
|
|
test('throws error when there are no releases', async () => {
|
|
octokit.rest.repos.listReleases.mockResolvedValue({data: []});
|
|
|
|
await expect(action.uploadReleaseAsset('1.0.0', 'release-asset.zip'))
|
|
.rejects
|
|
.toThrow('No release found for tag: 1.0.0');
|
|
});
|
|
|
|
test('throws error when release not found', async () => {
|
|
octokit.rest.repos.listReleases.mockResolvedValue({
|
|
data: [
|
|
{
|
|
id: 123,
|
|
tag_name: '1.0.0',
|
|
name: 'Release 1.0.0',
|
|
},
|
|
],
|
|
});
|
|
|
|
await expect(action.uploadReleaseAsset('1.1.0', 'release-asset.zip'))
|
|
.rejects
|
|
.toThrow('No release found for tag: 1.1.0');
|
|
});
|
|
});
|
|
|
|
describe('extensionDetails', () => {
|
|
test('extension details are returned', async () => {
|
|
core.getInput.mockReturnValue('1.2.3');
|
|
|
|
jest.spyOn(action, 'determinePhpBinary').mockResolvedValue('/usr/bin/php');
|
|
jest.spyOn(action, 'determineExtensionNameFromComposerJson').mockResolvedValue('foo');
|
|
jest.spyOn(action, 'determinePhpVersionFromPhpConfig').mockResolvedValue('8.1');
|
|
jest.spyOn(action, 'determineArchitecture').mockResolvedValue('x86_64');
|
|
jest.spyOn(action, 'determineOperatingSystem').mockResolvedValue('linux');
|
|
jest.spyOn(action, 'determineLibcFlavour').mockResolvedValue('glibc');
|
|
jest.spyOn(action, 'determinePhpDebugMode').mockResolvedValue('');
|
|
jest.spyOn(action, 'determineZendThreadSafeMode').mockResolvedValue('');
|
|
|
|
expect(await action.extensionDetails())
|
|
.toEqual({
|
|
releaseTag: '1.2.3',
|
|
extSoFile: 'foo.so',
|
|
extPackageName: 'php_foo-1.2.3_php8.1-x86_64-linux-glibc.zip',
|
|
});
|
|
});
|
|
|
|
test('extension details are returned for debug/zts', async () => {
|
|
core.getInput.mockReturnValue('1.2.3');
|
|
|
|
jest.spyOn(action, 'determinePhpBinary').mockResolvedValue('/usr/bin/php');
|
|
jest.spyOn(action, 'determineExtensionNameFromComposerJson').mockResolvedValue('foo');
|
|
jest.spyOn(action, 'determinePhpVersionFromPhpConfig').mockResolvedValue('8.1');
|
|
jest.spyOn(action, 'determineArchitecture').mockResolvedValue('x86_64');
|
|
jest.spyOn(action, 'determineOperatingSystem').mockResolvedValue('linux');
|
|
jest.spyOn(action, 'determineLibcFlavour').mockResolvedValue('glibc');
|
|
jest.spyOn(action, 'determinePhpDebugMode').mockResolvedValue('-debug');
|
|
jest.spyOn(action, 'determineZendThreadSafeMode').mockResolvedValue('-zts');
|
|
|
|
expect(await action.extensionDetails())
|
|
.toEqual({
|
|
releaseTag: '1.2.3',
|
|
extSoFile: 'foo.so',
|
|
extPackageName: 'php_foo-1.2.3_php8.1-x86_64-linux-glibc-debug-zts.zip',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('main', () => {
|
|
test('main builds and uploads extension with default build path', async () => {
|
|
jest.spyOn(action, 'extensionDetails').mockResolvedValue({
|
|
releaseTag: '1.2.3',
|
|
extSoFile: 'foo.so',
|
|
extPackageName: 'php_foo-1.2.3_php8.1-x86_64-linux-glibc-debug-zts.zip',
|
|
});
|
|
jest.spyOn(action, 'buildExtension').mockResolvedValue();
|
|
jest.spyOn(action, 'uploadReleaseAsset').mockResolvedValue();
|
|
jest.spyOn(exec, 'exec').mockResolvedValue();
|
|
core.getInput.mockImplementation((name) => {
|
|
if (name === 'build-path') return '.';
|
|
return '';
|
|
});
|
|
|
|
await action.main();
|
|
|
|
expect(action.buildExtension).toHaveBeenCalled();
|
|
expect(action.uploadReleaseAsset).toHaveBeenCalledWith('1.2.3', 'php_foo-1.2.3_php8.1-x86_64-linux-glibc-debug-zts.zip');
|
|
expect(exec.exec).toHaveBeenCalledWith('ls', ['-l', 'modules']);
|
|
expect(exec.exec).toHaveBeenCalledWith('zip', ['-j', 'php_foo-1.2.3_php8.1-x86_64-linux-glibc-debug-zts.zip', 'modules/foo.so']);
|
|
expect(core.setOutput).toHaveBeenCalledWith('package-path', 'php_foo-1.2.3_php8.1-x86_64-linux-glibc-debug-zts.zip');
|
|
});
|
|
|
|
test('main builds and uploads extension with custom build path', async () => {
|
|
jest.spyOn(action, 'extensionDetails').mockResolvedValue({
|
|
releaseTag: '1.2.3',
|
|
extSoFile: 'foo.so',
|
|
extPackageName: 'php_foo-1.2.3_php8.1-x86_64-linux-glibc-debug-zts.zip',
|
|
});
|
|
jest.spyOn(action, 'buildExtension').mockResolvedValue();
|
|
jest.spyOn(action, 'uploadReleaseAsset').mockResolvedValue();
|
|
jest.spyOn(exec, 'exec').mockResolvedValue();
|
|
core.getInput.mockImplementation((name) => {
|
|
if (name === 'build-path') return 'src/php/ext/grpc';
|
|
return '';
|
|
});
|
|
|
|
await action.main();
|
|
|
|
expect(action.buildExtension).toHaveBeenCalled();
|
|
expect(action.uploadReleaseAsset).toHaveBeenCalledWith('1.2.3', 'php_foo-1.2.3_php8.1-x86_64-linux-glibc-debug-zts.zip');
|
|
expect(exec.exec).toHaveBeenCalledWith('ls', ['-l', 'src/php/ext/grpc/modules']);
|
|
expect(exec.exec).toHaveBeenCalledWith('zip', ['-j', 'php_foo-1.2.3_php8.1-x86_64-linux-glibc-debug-zts.zip', 'src/php/ext/grpc/modules/foo.so']);
|
|
expect(core.setOutput).toHaveBeenCalledWith('package-path', 'php_foo-1.2.3_php8.1-x86_64-linux-glibc-debug-zts.zip');
|
|
});
|
|
});
|