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

Test: TypeScript type specification strength tests #32905

Merged
merged 4 commits into from
Mar 15, 2019
Merged
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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"test:ui:runner": "node scripts/functional_test_runner",
"test:server": "grunt test:server",
"test:coverage": "grunt test:coverage",
"typespec": "typings-tester --config x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json x-pack/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts",
"checkLicenses": "grunt licenses --dev",
"build": "node scripts/build --all-platforms",
"start": "node --trace-warnings --trace-deprecation scripts/kibana --dev ",
Expand Down Expand Up @@ -409,6 +410,7 @@
"tslint-microsoft-contrib": "^6.0.0",
"tslint-plugin-prettier": "^2.0.0",
"typescript": "^3.3.3333",
"typings-tester": "^0.3.2",
"vinyl-fs": "^3.0.2",
"xml2js": "^0.4.19",
"xmlbuilder": "9.0.4",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { select } from '../../select';
import { Json, Selector } from '../..';

/*

Type checking isn't too useful if future commits can accidentally weaken the type constraints, because a
TypeScript linter will not complain - everything that passed before will continue to pass. The coder
will not have feedback that the original intent with the typing got compromised. To declare the intent
via passing and failing type checks, test cases are needed, some of which designed to expect a TS pass,
some of them to expect a TS complaint. It documents intent for peers too, as type specs are a tough read.

Run compile-time type specification tests in the `kibana` root with:

yarn typespec

Test "cases" expecting to pass TS checks are not annotated, while ones we want TS to complain about
are prepended with the comment

// typings:expect-error

The test "suite" and "cases" are wrapped in IIFEs to prevent linters from complaining about the unused
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we used module.typespec.ts and told tslint to ignore .typespec.ts files? Could we avoid the IIFE gymnastics and make the variable declarations a little more straightforward?

let plain: Json;
// numbers are OK
plain = 1;
plain = NaN;
plain = Infinity;
plain = -Infinity;
plain = Math.pow(2, 6);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@spalger thanks for the comments!

Re module.typespec.ts I'm not familiar with it, a one-liner PR against this branch or some other pointer would be welcome, or in theory, this conversion could be done as a subsequent PR by your or someone who has the chops.

Re IIFE: my purpose was to 1) make it a bit like describe / it; 2) use informative function names for some structure without resorting to comment lines, 3) limit the scope of these variables so the reader can see what's what; 4) allow the movement of tested units (now IIFEs) with structured editing as done in Lisp code (whereas if it's line based, the logical boundaries are unclear so it's super error prone to change the sequence of anything or rename something etc.).

I could solve these with either IIFEs (or analogous) or by using separate files, but I didn't want to make files too small - it's nice to cover a sensible unit in one file (ymmv though). Also, maybe someone spends the time and defines describe / it functions for making it look even more like unit tests, well assuming it's a goal at all 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... taking another look, I agree it'd be less tedious if I used eg. let plain: Json etc. instead of passing these as typed IIFE args, which indeed complexifies like the WaPo 😸 This is why I stuck with it (welcome to try and suggest alternatives):

  • linter will complain that I bind values but don't actually use the value (I could return the value but then the simplicity of returning void is gone, so it moves cruft from here to there)
  • with the non-value-binding let, as there's no initial value, it won't actually adhere to the type unless the type includes the undefined value (I'm not sure if it's a tslint issue or if I just found it weird to have a purportedly type X variable which then was undefined which is not allowed by the typespec)

One more thing I planned with the functions (which are immediately invoked right now, but don't have to be): potentially reuse them. Eg. pass on various parameters to try things in different combinations.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All great points, I'm open to refining the approach in subsequent PRs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @spalger, linked your suggestions in the PR description so we see them all in one place, now or post-merge.

binding. It can be structured internally as desired.

*/

((): void => {
/**
* TYPE TEST SUITE
*/

(function jsonTests(plain: Json): void {
// numbers are OK
plain = 1;
plain = NaN;
plain = Infinity;
plain = -Infinity;
plain = Math.pow(2, 6);
// other JSON primitive types are OK
plain = false;
plain = 'hello';
plain = null;
// structures made of above and of structures are OK
plain = {};
plain = [];
plain = { a: 1 };
plain = [0, null, false, NaN, 3.14, 'one more'];
plain = { a: { b: 5, c: { d: [1, 'a', -Infinity, null], e: -1 }, f: 'b' }, g: false };

// typings:expect-error
plain = undefined; // it's undefined
// typings:expect-error
plain = a => a; // it's a function
// typings:expect-error
plain = [new Date()]; // it's a time
// typings:expect-error
plain = { a: Symbol('haha') }; // symbol isn't permitted either
// typings:expect-error
plain = window || void 0;
// typings:expect-error
plain = { a: { b: 5, c: { d: [1, 'a', undefined, null] } } }; // going deep into the structure

return; // jsonTests
})(null);

(function selectTests(selector: Selector): void {
selector = select((a: Json) => a); // one arg
selector = select((a: Json, b: Json): Json => `${a} and ${b}`); // more args
selector = select(() => 1); // zero arg
selector = select((...args: Json[]) => args); // variadic

// typings:expect-error
selector = (a: Json) => a; // not a selector
// typings:expect-error
selector = select(() => {}); // should yield a JSON value, but it returns void
// typings:expect-error
selector = select((x: Json) => ({ a: x, b: undefined })); // should return a Json

return; // selectTests
})(select((a: Json) => a));

return; // test suite
})();
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

// linear algebra
type f64 = number; // eventual AssemblyScript compatibility; doesn't hurt with vanilla TS either
type f = f64; // shorthand

Expand All @@ -15,16 +16,36 @@ export type transformMatrix2d = [f, f, f, f, f, f, f, f, f] &
export type transformMatrix3d = [f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f] &
ReadonlyArray<f> & { __nominal: 'transformMatrix3d' };

// plain, JSON-bijective value
export type Json = JsonPrimitive | JsonArray | JsonMap;
type JsonPrimitive = null | boolean | number | string;
interface JsonArray extends Array<Json> {}
interface JsonMap extends IMap<Json> {}
interface IMap<T> {
[key: string]: T;
}

// state object
export type State = JsonMap & WithActionId;
export type ActionId = number;
interface WithActionId {
primaryUpdate: { type: string; payload: { uid: ActionId; [propName: string]: Json } };
[propName: string]: Json; // allow other arbitrary props
}

// reselect-based data flow
export type PlainFun = (...args: Json[]) => Json;
export type Selector = (...fns: Resolve[]) => Resolve;
type Resolve = ((obj: State) => Json);

//
export interface Meta {
silent: boolean;
}
export type ActionId = number;
export type TypeName = string;
export type NodeResult = any;
export type Payload = any;
export type NodeFunction = (...args: any[]) => any;
export type UpdaterFunction = (arg: NodeResult) => NodeResult;
export type Payload = JsonMap;
export type UpdaterFunction = (arg: State) => State;
export type ChangeCallbackFunction = (
{ type, state }: { type: TypeName; state: NodeResult },
{ type, state }: { type: TypeName; state: State },
meta: Meta
) => void;
2 changes: 1 addition & 1 deletion x-pack/plugins/canvas/public/lib/aeroelastic/matrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
*
*/

import { transformMatrix3d, vector3d } from './types';
import { transformMatrix3d, vector3d } from '.';

const NANMATRIX = [
NaN,
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/canvas/public/lib/aeroelastic/matrix2d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { transformMatrix2d, vector2d } from './types';
import { transformMatrix2d, vector2d } from '.';

export const ORIGIN = [0, 0, 1] as vector2d;

Expand Down
12 changes: 5 additions & 7 deletions x-pack/plugins/canvas/public/lib/aeroelastic/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { ActionId, NodeFunction, NodeResult } from './types';
import { ActionId, Json, PlainFun, Selector, State } from '.';

export const select = (fun: NodeFunction): NodeFunction => (...fns) => {
export const select = (fun: PlainFun): Selector => (...fns) => {
let prevId: ActionId = NaN;
let cache: NodeResult = null;
const old = (object: NodeResult): boolean =>
prevId === (prevId = object.primaryUpdate.payload.uid);
return (obj: NodeResult) =>
old(obj) ? cache : (cache = fun(...fns.map(f => f(obj) as NodeResult)));
let cache: Json = null;
const old = (object: State): boolean => prevId === (prevId = object.primaryUpdate.payload.uid);
return (obj: State) => (old(obj) ? cache : (cache = fun(...fns.map(f => f(obj) as State))));
};
6 changes: 3 additions & 3 deletions x-pack/plugins/canvas/public/lib/aeroelastic/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ import {
ActionId,
ChangeCallbackFunction,
Meta,
NodeResult,
Payload,
State,
TypeName,
UpdaterFunction,
} from './types';
} from '.';

let counter = 0 as ActionId;

export const createStore = (
initialState: NodeResult,
initialState: State,
updater: UpdaterFunction,
onChangeCallback: ChangeCallbackFunction
) => {
Expand Down
21 changes: 21 additions & 0 deletions x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"extends": "../../../../../tsconfig",
"compilerOptions": {
"module": "commonjs",
"lib": ["esnext", "dom"],
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": false,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"baseUrl": ".",
"paths": {
"layout/*": ["aeroelastic/*"]
},
"types": ["@kbn/x-pack/plugins/canvas/public/lib/aeroelastic"]
},
"exclude": ["node_modules", "**/*.spec.ts", "node_modules/@types/mocha"]
}
5 changes: 1 addition & 4 deletions x-pack/plugins/canvas/public/lib/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { transformMatrix3d } from './aeroelastic/types';
import { transformMatrix3d } from './aeroelastic';

// converts a transform matrix to a CSS string
export const matrixToCSS = (transformMatrix: transformMatrix3d): string =>
transformMatrix ? 'matrix3d(' + transformMatrix.join(',') + ')' : 'translate3d(0,0,0)';

// converts to string, and adds `px` if non-zero
export const px = (value: number): string => (value === 0 ? '0' : value + 'px');
3 changes: 2 additions & 1 deletion x-pack/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"test_utils/**/*"
],
"exclude": [
"test/**/*"
"test/**/*",
"**/typespec_tests.ts"
],
"compilerOptions": {
"outDir": ".",
Expand Down
14 changes: 8 additions & 6 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6675,7 +6675,7 @@ [email protected]:
resolved "https://registry.yarnpkg.com/commander/-/commander-0.6.1.tgz#fa68a14f6a945d54dbbe50d8cdb3320e9e3b1a06"
integrity sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=

commander@2, [email protected], commander@^2.11.0, commander@^2.12.1, commander@^2.19.0, commander@^2.9.0:
commander@2, [email protected], commander@^2.11.0, commander@^2.12.1, commander@^2.12.2, commander@^2.19.0, commander@^2.9.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==
Expand Down Expand Up @@ -23530,12 +23530,14 @@ typescript@^3.3.3333:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.3333.tgz#171b2c5af66c59e9431199117a3bcadc66fdcfd6"
integrity sha512-JjSKsAfuHBE/fB2oZ8NxtRTk5iGcg6hkYXMnZ3Wc+b2RSqejEqTaem11mHASMnFilHrax3sLK0GDzcJrekZYLw==

ua-parser-js@^0.7.18:
version "0.7.18"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed"
integrity sha512-LtzwHlVHwFGTptfNSgezHp7WUlwiqb0gA9AALRbKaERfxwJoiX0A73QbTToxteIAuIaFshhgIZfqK8s7clqgnA==
typings-tester@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/typings-tester/-/typings-tester-0.3.2.tgz#04cc499d15ab1d8b2d14dd48415a13d01333bc5b"
integrity sha512-HjGoAM2UoGhmSKKy23TYEKkxlphdJFdix5VvqWFLzH1BJVnnwG38tpC6SXPgqhfFGfHY77RlN1K8ts0dbWBQ7A==
dependencies:
commander "^2.12.2"

ua-parser-js@^0.7.9:
ua-parser-js@^0.7.18, ua-parser-js@^0.7.9:
version "0.7.19"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b"
integrity sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ==
Expand Down