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

Add jest-serializer module #5609

Merged
merged 3 commits into from
Feb 19, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 0 additions & 1 deletion packages/jest-cli/src/search_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import type {Glob, GlobalConfig, Path} from 'types/Config';
import type {Test} from 'types/TestRunner';
import type {ChangedFilesPromise} from 'types/ChangedFiles';

import fs from 'fs';
import path from 'path';
import micromatch from 'micromatch';
import DependencyResolver from 'jest-resolve-dependencies';
Expand Down
1 change: 1 addition & 0 deletions packages/jest-haste-map/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"fb-watchman": "^2.0.0",
"graceful-fs": "^4.1.11",
"jest-docblock": "^22.2.2",
"jest-serializer": "^22.3.0",
"jest-worker": "^22.2.2",
"micromatch": "^2.3.11",
"sane": "^2.0.0"
Expand Down
42 changes: 9 additions & 33 deletions packages/jest-haste-map/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {version as VERSION} from '../package.json';
import {worker} from './worker';
import crypto from 'crypto';
import EventEmitter from 'events';
import fs from 'graceful-fs';
import getMockName from './get_mock_name';
import getPlatformExtension from './lib/get_platform_extension';
// eslint-disable-next-line import/no-duplicates
Expand All @@ -25,7 +24,7 @@ import normalizePathSep from './lib/normalize_path_sep';
import os from 'os';
import path from 'path';
import sane from 'sane';
import v8 from 'v8';
import serializer from 'jest-serializer';
// eslint-disable-next-line import/default
import watchmanCrawl from './crawlers/watchman';
import WatchmanWatcher from './lib/watchman_watcher';
Expand Down Expand Up @@ -290,21 +289,14 @@ class HasteMap extends EventEmitter {
* 1. read data from the cache or create an empty structure.
*/
read(): InternalHasteMap {
if (v8.deserialize) {
// This may throw. `_buildFileMap` will catch it and create a new map.
const {version, hasteMap} = v8.deserialize(
fs.readFileSync(this._cachePath),
);
if (version !== process.versions.v8) {
throw new Error('jest-haste-map: v8 versions do not match.');
}
return removePrototypes(hasteMap);
} else {
const hasteMap = (JSON.parse(
fs.readFileSync(this._cachePath, 'utf8'),
): InternalHasteMap);
return removePrototypes(hasteMap);
// This may throw. `_buildFileMap` will catch it and create a new map.
const hasteMap: InternalHasteMap = serializer.readFileSync(this._cachePath);

for (const key in hasteMap) {
Object.setPrototypeOf(hasteMap[key], null);
}

return hasteMap;
}

readModuleMap(): ModuleMap {
Expand Down Expand Up @@ -534,17 +526,7 @@ class HasteMap extends EventEmitter {
* 4. serialize the new `HasteMap` in a cache file.
*/
_persist(hasteMap: InternalHasteMap) {
if (v8.serialize) {
fs.writeFileSync(
this._cachePath,
v8.serialize({
hasteMap,
version: process.versions.v8,
}),
);
} else {
fs.writeFileSync(this._cachePath, JSON.stringify(hasteMap), 'utf8');
}
serializer.writeFileSync(this._cachePath, hasteMap);
}

/**
Expand Down Expand Up @@ -913,12 +895,6 @@ class HasteMap extends EventEmitter {
}

const copy = object => Object.assign(Object.create(null), object);
const removePrototypes = object => {
for (const key in object) {
Object.setPrototypeOf(object[key], null);
}
return object;
};

HasteMap.H = H;
HasteMap.ModuleMap = HasteModuleMap;
Expand Down
3 changes: 3 additions & 0 deletions packages/jest-serializer/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
**/__mocks__/**
**/__tests__/**
src
86 changes: 86 additions & 0 deletions packages/jest-serializer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# jest-serializer

Module for serializing and deserializing object into memory and disk. By
default, the `v8` implementations are used, but if not present, it defaults to
`JSON` implementation. The V8 serializer has the advantage of being able to
serialize `Map`, `Set`, `undefined`, circular references, etc. It is also a
more compact format to be stored and sent over the wire.

## Install

```sh
$ yarn add jest-serializer
```

## API

Three kinds of API groups are exposed:

### In-memory serialization: `serialize` and `deserialize`

This set of functions take or return a `Buffer`. All the process happens in
memory. This is useful when willing to transfer over HTTP, TCP or via UNIX
pipes.

```javascript
import serializer from 'jest-serializer';
Copy link
Collaborator

Choose a reason for hiding this comment

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

Destructuring maybe?

import {serialize, deserialize} from 'jest-serializer';


const myObject = {
foo: 'bar',
baz: [0, true, '2', [], {}],
};

const buffer = serializer.serialize(myObject);
const myCopyObject = serializer.deserialize(buffer);
```

### Synchronous persistent filesystem: `readFileSync` and `writeFileSync`

This set of functions allow to send to disk a serialization result and retrieve
it back, in a synchronous way. It mimics the `fs` API so it looks familiar.

```javascript
import serializer from 'jest-serializer';

const myObject = {
foo: 'bar',
baz: [0, true, '2', [], {}],
};

const myFile = '/tmp/obj';

serializer.writeFileSync(myFile, myObject);
const myCopyObject = serializer.readFileSync(myFile);
```

### Asynchronous persistent filesystem: `readFile` and `writeFile`

Pretty similar to the synchronous one, but providing a callback. It also mimics
the `fs` API.

```javascript
import serializer from 'jest-serializer';

const myObject = {
foo: 'bar',
baz: [0, true, '2', [], {}],
};

const myFile = '/tmp/obj';

serializer.writeFile(myFile, myObject, (err) => {
if (err) {
console.error(err);
return;
}

serializer.readFile(myFile, (err, data) => {
if (err) {
console.error(err);
return;
}

const myCopyObject = data;
});
});
```
13 changes: 13 additions & 0 deletions packages/jest-serializer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "jest-serializer",
"version": "22.3.0",
"repository": {
"type": "git",
"url": "https://github.com/facebook/jest.git"
},
"license": "MIT",
"main": "build/index.js",
"dependencies": {
"merge-stream": "^1.0.1"
Copy link
Contributor

Choose a reason for hiding this comment

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

Where is used this dependency?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nowhere 😳I just copy-pasted the jest-worker one and forgot to remove.

}
}
92 changes: 92 additions & 0 deletions packages/jest-serializer/src/__tests__/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* Copyright (c) 2018-present, Facebook, Inc. All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Some Flow maybe?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah, pardon this is a test (so it's not necessary to flow it, but I like to mix these)

*/

'use strict';

import fs from 'fs';
import os from 'os';
import path from 'path';
import v8 from 'v8';

import serializer from '..';

const v8s = [
{
deserialize: v8.deserialize,
serialize: v8.serialize,
},
{
deserialize: undefined,
serialize: undefined,
},
];

const objs = [
3,
null,
[0, true, '2', [3.14, {}, null]],
{key1: 'foo', key2: 'bar', key3: {array: [null, {}]}},
];

const file = path.join(os.tmpdir(), '__jest-serialize-test__');

afterEach(() => {
try {
fs.unlinkSync(file);
} catch (err) {
// Do nothing if file does not exist.
}
});

// We execute the same suite of tests over multiple objects ("objs") and over
// multiple mocks of the V8 object ("v8s") so that we verify that all possible
// encodings and cases work.
v8s.forEach((mockV8, i) => {
describe('Using V8 implementation ' + i, () => {
jest.mock('v8', () => mockV8);

it('throws the error with an invalid serialization', done => {
// No chance this is a valid serialization, neither in JSON nor V8.
const invalidBuffer = Buffer.from([0x00, 0x55, 0xaa, 0xff]);

fs.writeFileSync(file, invalidBuffer);

expect(() => serializer.deserialize(invalidBuffer)).toThrow();
expect(() => serializer.readFileSync(file)).toThrow();

serializer.readFile(file, err => {
expect(err).toBeDefined();
done();
});
});

objs.forEach((obj, i) => {
describe('Object ' + i, () => {
it('serializes/deserializes in memory', () => {
const buf = serializer.serialize(obj);

expect(buf).toBeInstanceOf(Buffer);
expect(serializer.deserialize(buf)).toEqual(obj);
});

it('serializes/deserializes in disk, synchronously', () => {
serializer.writeFileSync(file, obj);
expect(serializer.readFileSync(file)).toEqual(obj);
});

it('serializes/deserializes in disk, asynchronously', done => {
serializer.writeFile(file, obj, err => {
serializer.readFile(file, (err, data) => {
expect(data).toEqual(obj);
done();
});
});
});
});
});
});
});
107 changes: 107 additions & 0 deletions packages/jest-serializer/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* Copyright (c) 2018-present, Facebook, Inc. All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

'use strict';

import fs from 'fs';
import v8 from 'v8';

import type {Path} from 'types/Config';

type IOCallback = (?Error, ?any) => void;

// JSON and V8 serializers are both stable when it comes to compatibility. The
// current JSON specification is well defined in RFC 8259, and V8 ensures that
// the versions are compatible by encoding the serialization version in the own
// generated buffer.

// In memory functions.

export function serialize(content: any): Buffer {
return v8.serialize
? v8.serialize(content)
: Buffer.from(JSON.stringify(content));
}

export function deserialize(buffer: Buffer): any {
return v8.deserialize
? v8.deserialize(buffer)
: Buffer.from(JSON.stringify(buffer.toString('utf8')));
}

// Synchronous filesystem functions.

export function readFileSync(file: Path): any {
return v8.deserialize
? v8.deserialize(fs.readFileSync(file))
: JSON.parse(fs.readFileSync(file, 'utf8'));
}

export function writeFileSync(file: Path, content: any) {
Copy link
Member

Choose a reason for hiding this comment

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

can you call this filePath?

return v8.serialize
? fs.writeFileSync(file, v8.serialize(content))
: fs.writeFileSync(file, JSON.stringify(content), 'utf8');
}

// Asynchronous filesystem functions.

export function readFile(file: Path, callback: IOCallback) {
Copy link
Member

@SimenB SimenB Feb 19, 2018

Choose a reason for hiding this comment

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

if we expose an async API, I'd prefer it to be promise instead of callback based

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just killed the async one 😜. But you are right; it should have been Promise based. I just followed the standard Node JS format.

The reason for killing it is because we don't really need it anywhere: neither in jest-haste-map, nor in jest-worker (coming soon), nor in Metro, where we will use this module as well.

if (v8.deserialize) {
fs.readFile(file, (err, data) => {
if (err) {
callback(err);
return;
}

try {
callback(null, v8.deserialize(data));
} catch (error) {
callback(error);
}
});
} else {
fs.readFile(file, 'utf8', (err, data) => {
if (err) {
callback(err);
return;
}

try {
callback(null, JSON.parse(data));
} catch (error) {
callback(error);
}
});
}
}

export function writeFile(file: Path, content: any, callback: IOCallback) {
if (v8.serialize) {
try {
fs.writeFile(file, v8.serialize(content), callback);
} catch (err) {
callback(err);
}
} else {
try {
fs.writeFile(file, JSON.stringify(content), callback);
} catch (err) {
callback(err);
}
}
}

export default {
deserialize,
readFile,
readFileSync,
serialize,
writeFile,
writeFileSync,
};