Skip to content

Commit

Permalink
chore(go): extract embedded runtime sanity check to _test file (#2568)
Browse files Browse the repository at this point in the history
In order to avoid wasting cycles on every application start, moved the
sanity test for embedded code being correct (checking size & SHA512 hash
matches expected values) to a new `_test.go` file, so this is run by
`go test`, but not during the actual runtime of any consuming app.
  • Loading branch information
RomainMuller authored Feb 17, 2021
1 parent 8ef0dc1 commit e4a4d3c
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 24 deletions.
1 change: 1 addition & 0 deletions packages/@jsii/go-runtime/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/jsii-calc/
*.generated.go
*.generated_test.go

*.js
*.d.ts
145 changes: 121 additions & 24 deletions packages/@jsii/go-runtime/build-tools/gen.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env npx ts-node

import { CodeMaker } from 'codemaker';
import { createHash } from 'crypto';
import { readdirSync, readFileSync, statSync } from 'fs';
import { resolve } from 'path';

Expand All @@ -15,6 +16,7 @@ const EMBEDDED_RUNTIME_ROOT = resolve(
const OUTPUT_DIR = resolve(__dirname, '..', 'jsii-runtime-go');

const RUNTIME_FILE = 'embeddedruntime.generated.go';
const RUNTIME_TEST_FILE = 'embeddedruntime.generated_test.go';
const VERSION_FILE = 'version.generated.go';

const code = new CodeMaker({ indentationLevel: 1, indentCharacter: '\t' });
Expand All @@ -23,8 +25,10 @@ code.openFile(RUNTIME_FILE);
code.line('package jsii');
code.line();
code.open('var embeddedruntime = map[string][]byte{');
const bytesPerLine = 16;
const fileSize: Record<string, number> = {};
const fileInfo: Record<
string,
{ readonly size: number; readonly hash: readonly string[] }
> = {};

(function emitFiles(directory: string, prefix?: string) {
for (const file of readdirSync(directory)) {
Expand All @@ -42,35 +46,83 @@ const fileSize: Record<string, number> = {};

const key = prefix ? `${prefix}/${file}` : file;

const byteSlice = getByteSlice(fullPath);
fileSize[key] = byteSlice.length;
const { byteSlice, hash } = getByteSlice(fullPath);
fileInfo[key] = {
size: byteSlice.length,
hash,
};
code.open(`${JSON.stringify(key)}: []byte{`);
for (let i = 0; i < byteSlice.length; i += bytesPerLine) {
const line = byteSlice.slice(i, i + bytesPerLine);
code.line(`${line.join(', ')},`);
}
formatBytes(code, byteSlice);
code.close('},');
}
})(EMBEDDED_RUNTIME_ROOT);

code.close('}');
code.line();
const mainKey = JSON.stringify(
Object.keys(fileSize).find((f) => f.endsWith('jsii-runtime.js')),
Object.keys(fileInfo).find((f) => f.endsWith('jsii-runtime.js')),
)!;
code.line(`const embeddedruntimeMain = ${mainKey}`);
code.closeFile(RUNTIME_FILE);

// This allows us to sanity-check we've generated correct data
code.openFile(RUNTIME_TEST_FILE);
code.line('package jsii');
code.line();
// This performs sanity tests upon initialization
code.open('func init() {');
for (const [file, size] of Object.entries(fileSize)) {
code.open(`if len(embeddedruntime[${JSON.stringify(file)}]) != ${size} {`);
code.line(
`panic("Embedded runtime file ${file} does not have expected size of ${size} bytes!")`,
);
code.close('}');
code.open('import (');
code.line('"crypto/sha512"');
code.line('"testing"');
code.close(')');
code.line();
code.openBlock('func TestEmbeddedruntime(t *testing.T)');

code.open(
't.Run("embeddedruntime[embeddedruntimeMain] exists", func(t *testing.T) {',
);
code.openBlock('if _, exists := embeddedruntime[embeddedruntimeMain]; !exists');
code.line(
't.Errorf("embeddedruntimeMain refers to non-existent file %s", embeddedruntimeMain)',
);
code.closeBlock();
code.close('})');

for (const [file, { size, hash }] of Object.entries(fileInfo)) {
code.line();
code.open(`t.Run("embeddedruntime[\\"${file}\\"]", func(t *testing.T) {`);

code.open('checkEmbeddedFile(');
code.line('t,');
code.line(`"${file}",`);
code.line(`${readableNumber(size)},`);
code.open('[sha512.Size]byte{');
formatBytes(code, hash);
code.close('},');
code.close(')');

code.close('})');
}
code.close('}');
code.closeFile(RUNTIME_FILE);
code.closeBlock();
code.line();
code.openBlock(
'func checkEmbeddedFile(t *testing.T, name string, expectedSize int, expectedHash [sha512.Size]byte)',
);
code.line('data := embeddedruntime[name]');
code.line();
code.line('size := len(data)');
code.openBlock('if size != expectedSize');
code.line(
't.Errorf("Size mismatch: expected %d bytes, got %d", expectedSize, size)',
);
code.closeBlock();
code.line();
code.line('hash := sha512.Sum512(data)');
code.openBlock('if hash != expectedHash');
code.line(
't.Errorf("SHA512 do not match:\\nExpected: %x\\nActual: %x", expectedHash, hash)',
);
code.closeBlock();
code.closeBlock();
code.closeFile(RUNTIME_TEST_FILE);

code.openFile(VERSION_FILE);
code.line('package jsii');
Expand All @@ -83,12 +135,57 @@ code.closeFile(VERSION_FILE);

code.save(OUTPUT_DIR).catch(console.error);

function getByteSlice(path: string) {
const fileData = readFileSync(path).toString('hex');
function getByteSlice(path: string): { byteSlice: string[]; hash: string[] } {
const rawData = readFileSync(path);
return {
byteSlice: toHexBytes(rawData),
hash: toHexBytes(createHash('SHA512').update(rawData).digest()),
};
}

function toHexBytes(rawData: Buffer): string[] {
const hexString = rawData.toString('hex');
const result = [];
for (let i = 0; i < fileData.length; i += 2) {
result.push(`0x${fileData[i]}${fileData[i + 1]}`);
for (let i = 0; i < hexString.length; i += 2) {
result.push(`0x${hexString[i]}${hexString[i + 1]}`);
}

return result;
}

function formatBytes(
code: CodeMaker,
byteSlice: readonly string[],
bytesPerLine = 16,
) {
for (let i = 0; i < byteSlice.length; i += bytesPerLine) {
const line = byteSlice.slice(i, i + bytesPerLine);
code.line(`${line.join(', ')},`);
}
}

/**
* Turns a integer into a "human-readable" format, adding an `_` thousand
* separator.
*
* @param val an integer to be formatted.
*
* @returns the formatted number with thousand separators.
*/
function readableNumber(val: number): string {
return val.toFixed(0).replace(
// This regex can be a little jarring, so it is annotated below with the
// corresponding explanation. It can also be explained in plain english:
// matches the position before any sequence of N consecutive digits (0-9)
// where N is a multiple of 3.
/**/ /\B(?=(\d{3})+(?!\d))/g,
// \B -- not a word boundary (i.e: not start of input)
// (?= ) -- positive lookahead (does not consume input)
// ( )+ -- repeated one or more times
// \d -- any digit (0-9)
// {3} -- repeated exactly 3 times
// (?! ) -- negative lookahead (does not consume input)
// \d -- any digit (0-9), negated by surrounding group
//
'_',
);
}

0 comments on commit e4a4d3c

Please sign in to comment.