Skip to content

Commit

Permalink
Merge pull request #450 from balena-io/improve-testing
Browse files Browse the repository at this point in the history
Improve testing of contracts
  • Loading branch information
flowzone-app[bot] authored Feb 5, 2025
2 parents b042ab7 + 072ab72 commit ccacd51
Show file tree
Hide file tree
Showing 14 changed files with 221 additions and 103 deletions.
7 changes: 0 additions & 7 deletions .eslintrc.yml

This file was deleted.

1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npx --no lint-staged
5 changes: 5 additions & 0 deletions .lintstagedrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"*.ts": [
"balena-lint --fix"
],
}
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
8 changes: 8 additions & 0 deletions .prettierrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const fs = require('fs');

module.exports = JSON.parse(
fs.readFileSync(
__dirname + '/node_modules/@balena/lint/config/.prettierrc',
'utf8',
),
);
6 changes: 0 additions & 6 deletions contracts/sw.os-image/balenaos/contract.json

This file was deleted.

3 changes: 1 addition & 2 deletions contracts/sw.os/balenaos/contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
{ "type": "arch.sw", "slug": "i386" },
{ "type": "arch.sw", "slug": "amd64" }
]
},
{ "type": "sw.os-image", "slug": "balena-image" }
}
]
}
4 changes: 0 additions & 4 deletions contracts/sw.os/resinos/contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@
{
"type": "arch.sw",
"slug": "armv7hf"
},
{
"type": "sw.os-image",
"slug": "resinos"
}
]
}
24 changes: 17 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,27 @@
},
"private": true,
"scripts": {
"test": "eslint scripts && node scripts/check-contracts.js"
"test": "npm run lint && npm run test:node",
"lint": "balena-lint tests",
"lint-fix": "balena-lint --fix tests",
"test:node": "mocha -r ts-node/register --reporter spec tests/**/*.spec.ts --timeout 30000"
},
"author": "Balena Inc. <[email protected]>",
"license": "Apache-2.0",
"devDependencies": {
"eslint": "^4.8.0",
"eslint-config-standard": "^10.2.1",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-node": "^5.2.0",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-standard": "^3.0.1"
"@balena/contrato": "^0.12.0",
"@balena/lint": "^9.1.3",
"@types/chai": "^4.3.20",
"@types/chai-as-promised": "^7.1.4",
"@types/mocha": "^10.0.10",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"husky": "^9.1.7",
"lint-staged": "^15.2.11",
"mocha": "^11.0.1",
"p-map": "^7.0.3",
"ts-node": "^10.9.2",
"typescript": "^5.7.2"
},
"versionist": {
"publishedAt": "2025-01-28T17:12:51.985Z"
Expand Down
34 changes: 0 additions & 34 deletions scripts/check-contracts.js

This file was deleted.

43 changes: 0 additions & 43 deletions scripts/utils.js

This file was deleted.

8 changes: 8 additions & 0 deletions tests/chai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';

chai.use(chaiAsPromised);

export default chai;

export const { expect } = chai;
162 changes: 162 additions & 0 deletions tests/contracts.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { expect } from './chai';
import * as path from 'path';
import { promises as fs } from 'fs';
import type { ContractObject } from '@balena/contrato';
import { Contract, Universe, query } from '@balena/contrato';

const CONTRACTS_PATH = path.join(__dirname, '..', 'contracts');

async function findFiles(
dir: string,
filter: (filePath: string) => boolean = () => true,
): Promise<string[]> {
const allFiles = await fs.readdir(dir, { recursive: true });
const filePaths: string[] = [];
for (const fileName of allFiles) {
const filePath = path.join(dir, fileName);
const stat = await fs.stat(filePath);
if (!stat.isDirectory() && filter(filePath)) {
filePaths.push(filePath);
}
}

return filePaths;
}

type ContractMeta = {
type: string;
source: ContractObject;
path: string;
};

async function readContracts(dir: string): Promise<ContractMeta[]> {
const allFiles = await findFiles(
dir,
(fileName) => path.extname(fileName) === '.json',
);

const { default: pMap } = await import('p-map');

return (
await pMap(
allFiles.values(),
async (file) => {
const contents = await fs.readFile(file, { encoding: 'utf8' });
const source = JSON.parse(contents);
return {
type: path.basename(path.dirname(path.dirname(file))),
source,
path: file,
};
},

{ concurrency: 10 },
)
).flat();
}

describe('Balena Base Contracts', function () {
let allContractsMeta: ContractMeta[];

before(async () => {
allContractsMeta = await readContracts(CONTRACTS_PATH);
});

it('contracts are stored in the right folder', function () {
for (const contractMeta of allContractsMeta) {
expect(
contractMeta.source.type,
`the contract type '${contractMeta.source.type}' does not match its parent folder '${contractMeta.type}'`,
).to.equal(contractMeta.type);
}
});

it('all children in the contract universe are satisfied', function () {
const allContracts = allContractsMeta.flatMap(({ source }) =>
Contract.build(source),
);

const universe = new Universe();
universe.addChildren(allContracts);

// The contracts universe is internally consistent
// if all the children requirements are satisfied
expect(universe.getAllNotSatisfiedChildRequirements()).to.deep.equal([]);
});

it('os/arch/device contract combinations are satisfied', function () {
const allContracts = allContractsMeta.flatMap(({ source }) =>
Contract.build(source),
);

const universe = new Universe();
universe.addChildren(allContracts);

const contexts = query(
universe,
{
'sw.os': {
cardinality: 1,
filter: {
type: 'object',
properties: { slug: { not: { enum: ['balena-os', 'resinos'] } } },
},
},
'arch.sw': 1,
'sw.blob': '1+',
'sw.stack-variant': 1,
'hw.device-type': 1,
},
{ type: 'meta.context' },
);

for (const context of contexts) {
const unmet = context.getAllNotSatisfiedChildRequirements();
expect(
unmet,
'Unsatisfied requirements for context: ' +
JSON.stringify(context, null, 2),
).to.deep.equal([]);
}
});

// Skipped as it takes too much time to calculate all combinations
// TODO: unskip this once we can improve the efficiency
// of contrato
it.skip('os/arch/device/stack contract combinations are satisfied', function () {
const allContracts = allContractsMeta.flatMap(({ source }) =>
Contract.build(source),
);

const universe = new Universe();
universe.addChildren(allContracts);

const contexts = query(
universe,
{
'sw.os': {
cardinality: 1,
filter: {
type: 'object',
properties: { slug: { not: { enum: ['balena-os', 'resinos'] } } },
},
},
'arch.sw': 1,
'sw.blob': '1+',
'sw.stack': 1,
'sw.stack-variant': 1,
'hw.device-type': 1,
},
{ type: 'meta.context' },
);

for (const context of contexts) {
const unmet = context.getAllNotSatisfiedChildRequirements();
expect(
unmet,
'Unsatisfied requirements for context: ' +
JSON.stringify(context, null, 2),
).to.deep.equal([]);
}
});
});
18 changes: 18 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"module": "Node16",
"moduleResolution": "node16",
"outDir": "build",
"noUnusedParameters": true,
"noUnusedLocals": true,
"sourceMap": true,
"strict": true,
"target": "es2022",
"declaration": true,
"skipLibCheck": true
},
"include": [
"lib/**/*.ts",
"tests/**/*.ts"
]
}

0 comments on commit ccacd51

Please sign in to comment.