8000 Add snippet testing on CI. (#1715) · tensorflow/tfjs-core@9960893 · GitHub
[go: up one dir, main page]

Skip to content
This repository was archived by the owner on Aug 15, 2019. It is now read-only.

Commit 9960893

Browse files
author
Nikhil Thorat
authored
Add snippet testing on CI. (#1715)
DEV Example failure: https://pantheon.corp.google.com/cloud-build/builds/6ee4fe0b-2c1a-4e7e-aaf9-0707f6889da3?project=learnjs-174218 Also fix errors with snippets - Add isNaN, isFinite, isInf chaining API - Don't ignore scripts. This allows layers to depend on this script.
1 parent 0b7256e commit 9960893

19 files changed

+291
-28
lines changed

.npmignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
.rpt2_cache/
33
src/**/*_test.ts
44
integration_tests/
5-
scripts/
65
models/
76
coverage/
87
package/

cloudbuild.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ steps:
2828
dir: 'src/backends/webgpu/'
2929
args: ['test-ci']
3030
waitFor: ['-']
31+
- name: 'node:10'
32+
entrypoint: 'yarn'
33+
args: ['test-snippets']
34+
id: 'test-snippets'
35+
waitFor: ['yarn']
3136
secrets:
3237
- kmsKeyName: projects/learnjs-174218/locations/global/keyRings/tfjs/cryptoKeys/enc
3338
secretEnv:

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@
6767
"test-node": "tsc && node dist/test_node.js",
6868
"test-node-ci": "node dist/test_node.js",
6969
"test-integration": "./scripts/test-integration.js",
70-
"test-ci": "./scripts/test-ci.sh"
70+
"test-ci": "./scripts/test-ci.sh",
71+
"test-snippets": "ts-node ./scripts/test_snippets/test_snippets.ts"
7172
},
7273
"dependencies": {
7374
"@types/seedrandom": "2.4.27",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env node
2+
/**
3+
* @license
4+
* Copyright 2019 Google LLC. All Rights Reserved.
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
* =============================================================================
17+
*/
18+
import * as tf from '../../src/index';
19+
import {parseAndEvaluateSnippets} from './util';
20+
21+
parseAndEvaluateSnippets(tf);

scripts/test_snippets/util.ts

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/**
2+
* @license
3+
* Copyright 2019 Google LLC. All Rights Reserved.
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
* =============================================================================
16+
*/
17+
18+
import * as fs from 'fs';
19+
import * as path from 'path';
20+
import * as ts from 'typescript';
21+
22+
// Used for logging the number of snippets that have been found.
23+
let snippetCount = 0;
24+
25+
/**
26+
* Parse and evaluate snippets for the src/index.ts from where this script is
27+
* run.
28+
* @param tf The TensorFlow.js module to use when evaluating snippets. If used
29+
* outside core, this should be a union of core and the separate package.
30+
* This is unused here but is used in eval() of the snippets.
31+
*/
32+
// tslint:disable-next-line:no-any
33+
export async function parseAndEvaluateSnippets(tf: any) {
34+
const index = path.join(process.cwd(), 'src/index.ts');
35+
const tsconfigPath = path.join(process.cwd(), 'tsconfig.json');
36+
37+
// Use the same compiler options that we use to compile the library
38+
// here.
39+
const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, 'utf8'));
40+
41+
delete tsconfig.compilerOptions.moduleResolution;
42+
const program = ts.createProgram([index], tsconfig.compilerOptions);
43+
44+
const checker = program.getTypeChecker();
45+
46+
for (const sourceFile of program.getSourceFiles()) {
47+
if (!sourceFile.isDeclarationFile) {
48+
const children = sourceFile.getChildren();
49+
for (let i = 0; i < children.length; i++) {
50+
await visit(tf, checker, children[i], sourceFile);
51+
}
52+
}
53+
}
54+
55+
console.log(`Parsed and evaluated ${snippetCount} snippets successfully.`);
56+
}
57+
58+
async function visit(
59+
// tslint:disable-next-line:no-any
60+
tf: any, checker: ts.TypeChecker, node: ts.Node,
61+
sourceFile: ts.SourceFile) {
62+
const children = node.getChildren();
63+
for (let i = 0; i < children.length; i++) {
64+
await visit(tf, checker, children[i], sourceFile);
65+
}
66+
67+
if (ts.isClassDeclaration(node) || ts.isFunctionDeclaration(node) ||
68+
ts.isMethodDeclaration(node) || ts.isInterfaceDeclaration(node)) {
69+
const symbol = checker.getSymbolAtLocation(node.name);
70+
const jsdoc = getJSDocTag(symbol);
71+
if (jsdoc == null) {
72+
return;
73+
}
74+
// Ignore snippets of methods that have been marked with ignoreCI.
75+
if (jsdoc['ignoreCI']) {
76+
return;
77+
}
78+
79+
const documentation = symbol.getDocumentationComment(checker);
80+
if (documentation == null) {
81+
return;
82+
}
83+
for (let i = 0; i < documentation.length; i++) {
84+
const doc = documentation[i];
85+
const re = /```js.*?```/gs;
86+
const matches = re.exec(doc.text);
87+
if (matches == null) {
88+
return;
89+
}
90+
91+
for (let k = 0; k < matches.length; k++) {
92+
snippetCount++;
93+
94+
const match = matches[k];
95+
const lines = match.split('\n');
96+
const evalLines: string[] = [];
97+
for (let j = 0; j < lines.length; j++) {
98+
let line = lines[j];
99+
if (line.startsWith('```js')) {
100+
line = line.substring('```js'.length);
101+
}
102+
if (line.endsWith('```')) {
103+
line = line.substring(0, line.length - '```'.length);
104+
}
105+
line = line.trim();
106+
if (line.startsWith('*')) {
107+
line = line.substring(1).trim();
108+
}
109+
evalLines.push(line);
110+
}
111+
112+
const srcCode = evalLines.join('\n');
113+
114+
const evalString = '(async function runner() { try { ' + srcCode +
115+
'} catch (e) { reportError(e); } })()';
116+
117+
const oldLog = console.log;
118+
const oldWarn = console.warn;
119+
120+
const reportError = (e: string|Error) => {
121+
oldLog();
122+
oldLog(`Error executing snippet for ${symbol.name} at ${
123+
sourceFile.fileName}`);
124+
oldLog();
125+
oldLog(`\`\`\`js${srcCode}\`\`\``);
126+
oldLog();
127+
128+
console.error(e);
129+
process.exit(1);
130+
};
131+
132+
// Overrwrite console.log so we don't spam the console.
133+
console.log = (msg: string) => {};
134+
console.warn = (msg: string) => {};
135+
try {
136+
await eval(evalString);
137+
} catch (e) {
138+
reportError(e);
139+
}
140+
console.log = oldLog;
141+
console.warn = oldWarn;
142+
}
143+
}
144+
}
145+
}
146+
147+
interface JSDoc {
148+
namespace?: string;
149+
ignoreCI?: boolean;
150+
}
151+
152+
function getJSDocTag(symbol: ts.Symbol): JSDoc {
153+
const tags = symbol.getJsDocTags();
154+
for (let i = 0; i < tags.length; i++) {
155+
const jsdocTag = tags[i];
156+
if (jsdocTag.name === 'doc' && jsdocTag.text != null) {
157+
const json = convertDocStringToDocInfoObject(jsdocTag.text.trim());
158+
return json;
159+
}
160+
}
161+
return null;
162+
}
163+
164+
function convertDocStringToDocInfoObject(docString: string): JSDoc {
165+
const jsonString =
166+
docString.replace(/([a-zA-Z0-9]+):/g, '"$1":').replace(/\'/g, '"');
167+
return JSON.parse(jsonString);
168+
}

src/gradients.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -325,12 +325,12 @@ function variableGrads(f: () => Scalar, varList?: Variable[]):
325325
* ```js
326326
* const customOp = tf.customGrad((x, save) => {
327327
* // Save x to make sure it's available later for the gradient.
328-
* save({x});
328+
* save([x]);
329329
* // Override gradient of our custom x ^ 2 op to be dy * abs(x);
330330
* return {
331331
* value: x.square(),
332-
* // Note `saved.x` which points to the `x` we saved ealier.
333-
* gradFunc: (dy, saved) => [dy.mul(saved.x.abs())]
332+
* // Note `saved.x` which points to the `x` we saved earlier.
333+
* gradFunc: (dy, saved) => [dy.mul(saved[0].abs())]
334334
* };
335335
* });
336336
*

src/io/browser_files.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ IORouterRegistry.registerSaveRouter(browserDownloadsRouter);
261261
* const model = tf.sequential();
262262
* model.add(tf.layers.dense(
263263
* {units: 1, inputShape: [10], activation: 'sigmoid'}));
264-
* const saveResult = await model.save('downloads://mymodel'));
264+
* const saveResult = await model.save('downloads://mymodel');
265265
* // This will trigger downloading of two files:
266266
* // 'mymodel.json' and 'mymodel.weights.bin'.
267267
* console.log(saveResult);
@@ -283,7 +283,14 @@ IORouterRegistry.registerSaveRouter(browserDownloadsRouter);
283283
* @param config Additional configuration for triggering downloads.
284284
* @returns An instance of `BrowserDownloads` `IOHandler`.
285285
*/
286-
/** @doc {heading: 'Models', subheading: 'Loading', namespace: 'io'} */
286+
/**
287+
* @doc {
288+
* heading: 'Models',
289+
* subheading: 'Loading',
290+
* namespace: 'io',
291+
* ignoreCI: true
292+
* }
293+
*/
287294
export function browserDownloads(fileNamePrefix = 'model'): IOHandler {
288295
return new BrowserDownloads(fileNamePrefix);
289296
}
@@ -321,7 +328,14 @@ export function browserDownloads(fileNamePrefix = 'model'): IOHandler {
321328
* topology will be loaded from the JSON file above.
322329
* @returns An instance of `Files` `IOHandler`.
323330
*/
324-
/** @doc {heading: 'Models', subheading: 'Loading', namespace: 'io'} */
331+
/**
332+
* @doc {
333+
* heading: 'Models',
334+
* subheading: 'Loading',
335+
* namespace: 'io',
336+
* ignoreCI: true
337+
* }
338+
*/
325339
export function browserFiles(files: File[]): IOHandler {
326340
return new BrowserFiles(files);
327341
}

src/io/http.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,14 @@ IORouterRegistry.registerLoadRouter(httpRouter);
324324
* before the load is completed.
325325
* @returns An instance of `IOHandler`.
326326
*/
327-
/** @doc {heading: 'Models', subheading: 'Loading', namespace: 'io'} */
327+
/**
328+
* @doc {
329+
* heading: 'Models',
330+
* subheading: 'Loading',
331+
* namespace: 'io',
332+
* ignoreCI: true
333+
* }
334+
*/
328335
export function http(path: string, loadOptions?: LoadOptions): IOHandler {
329336
return new HTTPRequest(path, loadOptions);
330337
}

src/io/model_management.ts

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,14 @@ async function cloneModelInternal(
188188
* 'indexeddb://my/model/1'. Model artifacts info include type of the
189189
* model's topology, byte sizes of the topology, weights, etc.
190190
*/
191-
/** @doc {heading: 'Models', subheading: 'Management', namespace: 'io'} */
191+
/**
192+
* @doc {
193+
* heading: 'Models',
194+
* subheading: 'Management',
195+
* namespace: 'io',
196+
* ignoreCI: true
197+
* }
198+
*/
192199
async function listModels(): Promise<{[url: string]: ModelArtifactsInfo}> {
193200
const schemes = ModelStoreManagerRegistry.getSchemes();
194201
const out: {[url: string]: ModelArtifactsInfo} = {};
@@ -229,7 +236,14 @@ async function listModels(): Promise<{[url: string]: ModelArtifactsInfo}> {
229236
* is successful).
230237
* @throws Error if deletion fails, e.g., if no model exists at `path`.
231238
*/
232-
/** @doc {heading: 'Models', subheading: 'Management', namespace: 'io'} */
239+
/**
240+
* @doc {
241+
* heading: 'Models',
242+
* subheading: 'Management',
243+
* namespace: 'io',
244+
* ignoreCI: true
245+
* }
246+
*/
233247
async function removeModel(url: string): Promise<ModelArtifactsInfo> {
234248
const schemeAndPath = parseURL(url);
235249
const manager = ModelStoreManagerRegistry.getManager(schemeAndPath.scheme);
@@ -276,7 +290,14 @@ async function removeModel(url: string): Promise<ModelArtifactsInfo> {
276290
* @throws Error if copying fails, e.g., if no model exists at `sourceURL`, or
277291
* if `oldPath` and `newPath` are identical.
278292
*/
279-
/** @doc {heading: 'Models', subheading: 'Management', namespace: 'io'} */
293+
/**
294+
* @doc {
295+
* heading: 'Models',
296+
* subheading: 'Management',
297+
* namespace: 'io',
298+
* ignoreCI: true
299+
* }
300+
*/
280301
async function copyModel(
281302
sourceURL: string, destURL: string): Promise<ModelArtifactsInfo> {
282303
const deleteSource = false;
@@ -322,11 +343,18 @@ async function copyModel(
322343
* @throws Error if moving fails, e.g., if no model exists at `sourceURL`, or
323344
* if `oldPath` and `newPath` are identical.
324345
*/
325-
/** @doc {heading: 'Models', subheading: 'Management', namespace: 'io'} */
326-
async function moveModel(
327-
sourceURL: string, destURL: string): Promise<ModelArtifactsInfo> {
328-
const deleteSource = true;
329-
return await cloneModelInternal(sourceURL, destURL, deleteSource);
330-
}
346+
/**
347+
* @doc {
348+
* heading: 'Models',
349+
* subheading: 'Management',
350+
* namespace: 'io',
351+
* ignoreCI: true
352+
* }
353+
*/
354+
async function moveModel(sourceURL: string, destURL: string):
355+
Promise<ModelArtifactsInfo> {
356+
const deleteSource = true;
357+
return await cloneModelInternal(sourceURL, destURL, deleteSource);
358+
}
331359

332360
export {moveModel, copyModel, removeModel, listModels};

src/ops/browser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import {op} from './operation';
4040
* numChannels value less than 4 allows you to ignore channels. Defaults to
4141
* 3 (ignores alpha channel of input image).
4242
*/
43-
/** @doc {heading: 'Browser', namespace: 'browser'} */
43+
/** @doc {heading: 'Browser', namespace: 'browser', ignoreCI: true} */
4444
function fromPixels_(
4545
pixels: ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement,
4646
numChannels = 3): Tensor3D {

0 commit comments

Comments
 (0)
0