Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve testing of contracts #450

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
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',
),
);
23 changes: 16 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,26 @@
},
"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"
},
"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.9.4",
"@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",
"ts-node": "^10.9.2",
"typescript": "^5.7.2"
},
"versionist": {
"publishedAt": "2024-12-23T10:57:21.729Z"
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;
110 changes: 110 additions & 0 deletions tests/contracts.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('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('the contract 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"
]
}
Loading