spiegel-keyman/developer/src/kmc-package/test/package-compiler.tests.ts
Marc Durdin 724a8ea0f5 maint(developer): cleanup additional TestCompilerCallbacks references
Address a review comment from #15665 and cleanup additional references
that I missed the first time around, along with the `fs.readFileSync`
`Uint8Array` cast.

Follows: #15665
Test-bot: skip
2026-03-05 13:15:43 +01:00

310 lines
13 KiB
TypeScript

import 'mocha';
import * as fs from 'fs';
import * as path from 'path';
import { assert } from 'chai';
import JSZip from 'jszip';
import KEYMAN_VERSION from "@keymanapp/keyman-version";
import { KmpJsonFile } from '@keymanapp/common-types';
import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers';
import { makePathToFixture } from './helpers/index.js';
import { KmpCompiler } from '../src/compiler/kmp-compiler.js';
import { PackageCompilerMessages } from '../src/compiler/package-compiler-messages.js';
import { PackageValidation } from '../src/compiler/package-validation.js';
const debug = false;
describe('KmpCompiler', function () {
const MODELS : string[] = [
'example.qaa.sencoten',
'withfolders.qaa.sencoten',
];
const callbacks = new TestCompilerCallbacks(this);
let kmpCompiler: KmpCompiler = null;
this.beforeAll(async function() {
kmpCompiler = new KmpCompiler();
assert.isTrue(await kmpCompiler.init(callbacks, null));
});
for (const modelID of MODELS) {
const kpsPath = modelID.includes('withfolders') ?
makePathToFixture(modelID, 'source', `${modelID}.model.kps`) : makePathToFixture(modelID, `${modelID}.model.kps`);
const jsPath = modelID.includes('withfolders') ?
makePathToFixture(modelID, 'source', `${modelID}.model.js`) : makePathToFixture(modelID, `${modelID}.model.js`);
const kmpJsonIntermediatePath = makePathToFixture(modelID, `${modelID}.model.kmp.intermediate.json`);
const kmpJsonZippedPath = makePathToFixture(modelID, `${modelID}.model.kmp.zipped.json`);
const kmpJsonIntermediateFixture = JSON.parse(fs.readFileSync(kmpJsonIntermediatePath, 'utf-8'));
const kmpJsonZippedFixture = JSON.parse(fs.readFileSync(kmpJsonZippedPath, 'utf-8'));
// We override the fixture version so that we can compare with the compiler output
kmpJsonIntermediateFixture.system.keymanDeveloperVersion = KEYMAN_VERSION.VERSION;
//
// Test just the transform from kps to kmp.json
//
it(`should transform ${modelID}.model.kps to kmp.json`, function () {
let kmpJson: KmpJsonFile.KmpJsonFile;
assert.doesNotThrow(() => {
kmpJson = kmpCompiler.transformKpsToKmpObject(kpsPath);
});
// Test that the kmp.json data is identical
assert.deepEqual(kmpJson, kmpJsonIntermediateFixture);
// Note that in-memory kmp.json still contains paths in the files array.
// However, when building the .kmp, the final written kmp.json data is
// modified to strip paths.
// This was used when building initial test data
//fs.writeFileSync(kmpJsonPath, JSON.stringify(kmpJson), 'utf-8');
});
it(`should build a full .kmp for ${modelID}`, async function() {
const zip = JSZip();
// Build kmp.json in memory
const kmpJson: KmpJsonFile.KmpJsonFile = kmpCompiler.transformKpsToKmpObject(kpsPath);
// Build file.kmp in memory
const promise = kmpCompiler.buildKmpFile(kpsPath, kmpJson);
promise.then(data => {
// Check that file.kmp contains just 2 files - kmp.json and file.model.js,
// and that they match exactly what we expect
return zip.loadAsync(data, {checkCRC32: true}).then(zipFile => {
assert.equal(zipFile.length, 2);
return Promise.all([
zipFile.file("kmp.json").async('uint8array').then(kmpJsonOutput => {
assert.deepEqual(kmpJsonOutput, kmpJsonZippedFixture);
}),
zipFile.file(`${modelID}.model.js`).async('uint8array').then(modelJsFile => {
assert.deepEqual(modelJsFile, new Uint8Array(fs.readFileSync(jsPath)));
})
]);
});
});
return promise;
});
}
it('should generates a valid .kmp (zip) file', async function() {
this.timeout(10000); // building a zip file can sometimes be slow
const kpsPath = makePathToFixture('khmer_angkor', 'source', 'khmer_angkor.kps');
const kmpJsonRefPath = makePathToFixture('khmer_angkor', 'ref', 'kmp.json');
const kmpCompiler = new KmpCompiler();
assert.isTrue(await kmpCompiler.init(callbacks, null));
const kmpJsonFixture: KmpJsonFile.KmpJsonFile = JSON.parse(fs.readFileSync(kmpJsonRefPath, 'utf-8'));
// We override the fixture version so that we can compare with the compiler output
kmpJsonFixture.system.keymanDeveloperVersion = KEYMAN_VERSION.VERSION;
let kmpJson = null;
assert.doesNotThrow(() => {
kmpJson = kmpCompiler.transformKpsToKmpObject(kpsPath);
});
const kmpData = await kmpCompiler.buildKmpFile(kpsPath, kmpJson);
const zip = JSZip();
const jszip = await zip.loadAsync(kmpData);
assert.isNotNull(jszip.file('kmp.json')); // kmp.json should be present
// kmp file should contain the following files
const expectedFiles = [
'FONTLOG.txt', 'image002.png', 'KAK_Documentation_EN.pdf', 'KAK_Documentation_KH.pdf',
'keyboard_layout.png', 'khmer_angkor.js', 'khmer_angkor.kmx', 'khmer_angkor.kvk',
'khmer_busra_kbd.ttf', 'Mondulkiri-R.ttf', 'OFL.txt', 'OFL-FAQ.txt', 'readme.htm',
'splash.gif', 'welcome.htm',
'kmp.json', // standard .kmp metadata file
'kmp.inf', // legacy keyboard .kmp metdata file
];
assert.sameMembers(Object.entries(jszip.files).map(([s, o]) => o.name).sort(),
expectedFiles.sort(),
'khmer_angkor.kmp file should have exactly the expected files');
const kmpJsonData = JSON.parse(await jszip.file('kmp.json').async('string'));
assert.deepEqual(kmpJsonData, kmpJsonFixture);
});
/*
* Testing kmp.json generation
*/
it(`should transform a .kps file for a keyboard package to a correct kmp.json`, function () {
const kpsPath = makePathToFixture('kmp.json', 'ahom_star.kps');
const kmpJsonRefPath = makePathToFixture('kmp.json', 'kmp.json');
const kmpJsonActual = kmpCompiler.transformKpsToKmpObject(kpsPath);
if(kmpJsonActual == null) {
callbacks.printMessages();
assert.isNotNull(kmpJsonActual);
}
const kmpJsonFixture = JSON.parse(fs.readFileSync(kmpJsonRefPath, 'utf-8'));
assert.isNotNull(kmpJsonFixture);
// Blank out system.keymanDeveloperVersion which will vary
kmpJsonActual.system.keymanDeveloperVersion = '-';
kmpJsonFixture.system.keymanDeveloperVersion = '-';
// Strip file paths to basename for the actual (this is currently part of the
// zip phase and not unit testable without refactoring)
for(const file of kmpJsonActual.files) {
file.name = path.basename(file.name);
}
assert.deepEqual(kmpJsonActual, kmpJsonFixture);
});
it(`should support .kps 17.0 metadata correctly`, function () {
const kpsPath = makePathToFixture('kmp_2.0', 'khmer_angkor.kps');
const kmpJsonRefPath = makePathToFixture('kmp_2.0', 'kmp.json');
const kmpJsonActual = kmpCompiler.transformKpsToKmpObject(kpsPath);
if(kmpJsonActual == null) {
callbacks.printMessages();
assert.isNotNull(kmpJsonActual);
}
const kmpJsonFixture = JSON.parse(fs.readFileSync(kmpJsonRefPath, 'utf-8'));
assert.isNotNull(kmpJsonFixture);
// Blank out system.keymanDeveloperVersion which will vary
kmpJsonActual.system.keymanDeveloperVersion = '-';
kmpJsonFixture.system.keymanDeveloperVersion = '-';
// Strip file paths to basename for the actual (this is currently part of the
// zip phase and not unit testable without refactoring)
for(const file of kmpJsonActual.files) {
file.name = path.basename(file.name);
}
assert.deepEqual(kmpJsonActual, kmpJsonFixture);
});
/*
* Testing Warnings and Errors
*/
it('should warn on absolute paths', async function() {
this.timeout(10000); // building a zip file can sometimes be slow
const kpsPath = makePathToFixture('absolute_path', 'source', 'absolute_path.kps');
const kmpCompiler = new KmpCompiler();
assert.isTrue(await kmpCompiler.init(callbacks, null));
let kmpJson: KmpJsonFile.KmpJsonFile = null;
assert.doesNotThrow(() => {
kmpJson = kmpCompiler.transformKpsToKmpObject(kpsPath);
});
assert.isNotNull(kmpJson);
await assert.isNull(kmpCompiler.buildKmpFile(kpsPath, kmpJson));
if(debug) callbacks.printMessages();
assert.lengthOf(callbacks.messages, 2);
assert.deepEqual(callbacks.messages[0].code, PackageCompilerMessages.WARN_AbsolutePath);
assert.deepEqual(callbacks.messages[1].code, PackageCompilerMessages.ERROR_FileDoesNotExist);
});
// Testing path normalization
it('should normalize DOS pathnames from \\ to /', async function() {
// this.timeout(10000); // building a zip file can sometimes be slow
const kpsPath = makePathToFixture('normalize_paths', 'source', 'khmer_angkor.kps');
const kmpCompiler = new KmpCompiler();
assert.isTrue(await kmpCompiler.init(callbacks, null));
let kmpJson: KmpJsonFile.KmpJsonFile = null;
assert.doesNotThrow(() => {
kmpJson = kmpCompiler.transformKpsToKmpObject(kpsPath);
});
callbacks.printMessages();
assert.lengthOf(callbacks.messages, 0);
/*
<ExecuteProgram>exe\myprogram.exe</ExecuteProgram>
<ReadMeFile>..\source\readme.htm</ReadMeFile>
<GraphicFile>..\source\splash.gif</GraphicFile>
<WelcomeFile>..\source\welcome.htm</WelcomeFile>
<MSIFileName>msi\myprogram.msi</MSIFileName>
*/
assert.equal(kmpJson.options.executeProgram, 'exe/myprogram.exe');
assert.equal(kmpJson.options.readmeFile, '../source/readme.htm');
assert.equal(kmpJson.options.graphicFile, '../source/splash.gif');
assert.equal(kmpJson.options.welcomeFile, '../source/welcome.htm');
assert.equal(kmpJson.options.msiFilename, 'msi/myprogram.msi');
assert.equal(kmpJson.files[0].name, '../build/khmer_angkor.js');
/*
<OSKFont>..\shared\fonts\khmer\busrakbd\khmer_busra_kbd.ttf</OSKFont>
<DisplayFont>..\shared\fonts\khmer\mondulkiri\Mondulkiri-R.ttf</DisplayFont>
*/
// These are stripped to basename; note that there may be platform
// differences
assert.equal(kmpJson.keyboards[0].oskFont, 'khmer_busra_kbd.ttf');
assert.equal(kmpJson.keyboards[0].displayFont, 'Mondulkiri-R.ttf');
});
//
// Test some invalid package metadata
//
it(`should load a package with missing keyboard ID metadata`, function () {
const kmpJson = kmpCompiler.transformKpsToKmpObject(makePathToFixture('invalid', 'missing_keyboard_id.kps'));
assert.isNull(kmpJson); // with a missing keyboard_id, the package shouldn't load, but it shouldn't crash either
assert.deepEqual(callbacks.messages[0].code, PackageCompilerMessages.ERROR_MissingKeyboardId);
});
it(`should load a package with missing model ID metadata`, function () {
const kmpJson = kmpCompiler.transformKpsToKmpObject(makePathToFixture('invalid', 'missing_model_id.kps'));
assert.isNull(kmpJson); // with a missing model_id, the package shouldn't load, but it shouldn't crash either
assert.deepEqual(callbacks.messages[0].code, PackageCompilerMessages.ERROR_MissingModelId);
});
it(`should load a package with missing keyboard name metadata`, function () {
const kmpJson = kmpCompiler.transformKpsToKmpObject(makePathToFixture('invalid', 'missing_keyboard_name.kps'));
assert.equal(kmpJson.keyboards[0].name, 'version 4'); // picks up example.kmx's name
});
it(`should load a package with missing keyboard version metadata`, function () {
const kmpJson = kmpCompiler.transformKpsToKmpObject(makePathToFixture('invalid', 'missing_keyboard_version.kps'));
assert.equal(kmpJson.keyboards[0].version, '4.0'); // picks up example.kmx's version
});
it(`should handle a range of valid BCP47 tags`, function () {
const inputFilename = makePathToFixture('bcp47', 'valid_bcp47.kps');
const outputFilename = makePathToFixture('bcp47', 'valid_bcp47.kmp');
const kmpJson = kmpCompiler.transformKpsToKmpObject(inputFilename);
assert.isNotNull(kmpJson);
const validation = new PackageValidation(callbacks, {});
assert.isTrue(validation.validate(inputFilename, outputFilename, kmpJson));
});
it(`should reject an invalid BCP47 tag`, function () {
const inputFilename = makePathToFixture('bcp47', 'invalid_bcp47_1.kps');
const outputFilename = makePathToFixture('bcp47', 'invalid_bcp47_1.kmp');
const kmpJson = kmpCompiler.transformKpsToKmpObject(inputFilename);
assert.isNotNull(kmpJson);
const validation = new PackageValidation(callbacks, {});
assert.isFalse(validation.validate(inputFilename, outputFilename, kmpJson));
});
it(`should reject a package that contains itself`, function () {
const inputFilename = makePathToFixture('invalid', 'error_package_must_not_contain_itself.kps');
const outputFilename = makePathToFixture('invalid', 'error_package_must_not_contain_itself.kmp');
const kmpJson = kmpCompiler.transformKpsToKmpObject(inputFilename);
assert.isNotNull(kmpJson);
const validation = new PackageValidation(callbacks, {});
assert.isFalse(validation.validate(inputFilename, outputFilename, kmpJson));
assert.equal(callbacks.messages[0].code, PackageCompilerMessages.ERROR_PackageMustNotContainItself);
});
});