Skip to content

Commit

Permalink
Improve testing of contracts
Browse files Browse the repository at this point in the history
- Migrates contracts to typescript
- Parallelizes scanning of folders and loading contracts
- Adds validation for internal consistency of the contract universe

Change-type: patch
  • Loading branch information
pipex committed Dec 13, 2024
1 parent 474ab2d commit ce4e4d7
Show file tree
Hide file tree
Showing 11 changed files with 190 additions and 108 deletions.
7 changes: 0 additions & 7 deletions .eslintrc.yml

This file was deleted.

4 changes: 4 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

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',
),
);
59 changes: 35 additions & 24 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
{
"name": "contracts",
"version": "2.0.118",
"description": "Balena Base Contracts",
"repository": {
"type": "git",
"url": "[email protected]:balena-io/contracts.git"
},
"private": true,
"scripts": {
"test": "eslint scripts && node scripts/check-contracts.js"
},
"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"
},
"versionist": {
"publishedAt": "2024-12-04T16:14:01.315Z"
}
"name": "contracts",
"version": "2.0.118",
"description": "Balena Base Contracts",
"repository": {
"type": "git",
"url": "[email protected]:balena-io/contracts.git"
},
"private": true,
"scripts": {
"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"
},
"author": "Balena Inc. <[email protected]>",
"license": "Apache-2.0",
"devDependencies": {
"@balena/contrato": "^0.9.4",
"@balena/lint": "^9.1.3",
"@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",
"ts-node": "^10.9.2",
"typescript": "^5.7.2"
},
"versionist": {
"publishedAt": "2024-12-04T16:14:01.315Z"
},
"dependencies": {
"@types/chai": "^4.2.18"
}
}
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;
110 changes: 110 additions & 0 deletions tests/validate.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { expect } from './chai';
import * as path from 'path';
import { promises as fs } from 'fs';
import type { ContractObject } from '@balena/contrato';
import { Contract } from '@balena/contrato';

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

async function findFiles(
dir: string,
filter: (fileName: string, filePath: string) => boolean = () => true,
): Promise<string[]> {
const dirFiles = await fs.readdir(dir);

const filePaths: string[] = [];
const dirPaths: string[] = [];
for (const fileName of dirFiles) {
const filePath = path.join(dir, fileName);

const stat = await fs.stat(filePath);
if (stat.isDirectory()) {
dirPaths.push(filePath);
} else if (filter(fileName, filePath)) {
filePaths.push(filePath);
}
}

const allFiles = await Promise.all(dirPaths.map((d) => findFiles(d, filter)));
return filePaths.concat(...allFiles);
}

async function concurrentForEach<T>(
it: IterableIterator<T>,
fn: (t: T) => Promise<void>,
concurrency = 1,
) {
const run = async () => {
const next = it.next();
if (next.value && !next.done) {
await fn(next.value);
await run();
}
};
const runs = [];
for (let i = 0; i < concurrency; i++) {
runs.push(run());
}
await Promise.all(runs);
}

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 meta: ContractMeta[] = [];
await concurrentForEach(
allFiles.values(),
async (file) => {
const contents = await fs.readFile(file, { encoding: 'utf8' });
const source = JSON.parse(contents);
meta.push({
type: path.basename(path.dirname(path.dirname(file))),
source,
path: file,
});
},
10,
);

return meta;
}

describe('contract validation', function () {
let allContractsMeta: ContractMeta[];

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

it('contracts are stored in the correct 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('contracts universe is internally consistent', function () {
const allContracts = allContractsMeta
.map(({ source }) => Contract.build(source))
.flat();

const universe = new Contract({ type: 'meta.universe' });
universe.addChildren(allContracts);

// The contracts universe is internally consistent
// if all the children requirements are satisfied
expect(universe.getAllNotSatisfiedChildRequirements()).to.equal([]);
expect(universe.areChildrenSatisfied()).to.be.true;
});
});
19 changes: 19 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"module": "Node16",
"moduleResolution": "node16",
"outDir": "build",
"noUnusedParameters": true,
"noUnusedLocals": true,
"removeComments": true,
"sourceMap": true,
"strict": true,
"target": "es2022",
"declaration": true,
"skipLibCheck": true
},
"include": [
"lib/**/*.ts",
"tests/**/*.ts"
]
}

0 comments on commit ce4e4d7

Please sign in to comment.