Skip to content

Commit

Permalink
Merge c288d42 into afe2971
Browse files Browse the repository at this point in the history
  • Loading branch information
piotr-gawron committed Jun 14, 2016
2 parents afe2971 + c288d42 commit e454971
Show file tree
Hide file tree
Showing 7 changed files with 293 additions and 9 deletions.
43 changes: 43 additions & 0 deletions src/main/AbstractFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* AbstractFile is an abstract representation of a file. There are two implementation:
* 1. RemoteFile - representation of a file on a remote server which can be
* fetched in chunks, e.g. using a Range request.
* 2. LoclaStringFile is a representation of a file that was created from input string.
* Used for testing and small input files.
* @flow
*/
'use strict';

//import Q from 'q';

class AbstractFile {
constructor() {
//how to prevent instantation of this class???
//this code doesn't pass npm run flow
// if (new.target === AbstractFile) {
// throw new TypeError("Cannot construct AbstractFile instances directly");
// }
}

getBytes(start: number, length: number):Object {//: Q.Promise<ArrayBuffer> {
throw new TypeError("Method getBytes is not implemented");
}

// Read the entire file -- not recommended for large files!
getAll():Object {//: Q.Promise<ArrayBuffer> {
throw new TypeError("Method getAll is not implemented");
}

// Reads the entire file as a string (not an ArrayBuffer).
// This does not use the cache.
getAllString():Object {//: Q.Promise<string> {
throw new TypeError("Method getAllString is not implemented");
}

// Returns a promise for the number of bytes in the remote file.
getSize():Object {//: Q.Promise<number> {
throw new TypeError("Method getSize is not implemented");
}
}

module.exports = AbstractFile;
66 changes: 66 additions & 0 deletions src/main/LocalStringFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* LocalStringFile is a representation of a file that was created from input string. Used for testing and small input files.
* @flow
*/
'use strict';

import Q from 'q';
import AbstractFile from './AbstractFile';

class LocalStringFile extends AbstractFile {
fileLength: number;
content: string; //content of this "File"
buffer: ArrayBuffer;

constructor(content: string) {
super();
this.fileLength = content.length;
this.buffer = new ArrayBuffer(content.length); // 2 bytes for each char
this.content = content;

var bufView = new Uint8Array(this.buffer);
for (var i=0; i < this.fileLength; i++) {
bufView[i] = content.charCodeAt(i);
}
}

getBytes(start: number, length: number): Q.Promise<ArrayBuffer> {
if (length < 0) {
return Q.reject(`Requested <0 bytes (${length})`);
}

// If the remote file length is known, clamp the request to fit within it.
var stop = start + length - 1;
if (this.fileLength != -1) {
stop = Math.min(this.fileLength - 1, stop);
}

// First check the cache.
var buf = this.getFromCache(start, stop);
return Q.when(buf);
}

// Read the entire file -- not recommended for large files!
getAll(): Q.Promise<ArrayBuffer> {
var buf = this.getFromCache(0, this.fileLength - 1);
return Q.when(buf);
}

// Reads the entire file as a string (not an ArrayBuffer).
// This does not use the cache.
getAllString(): Q.Promise<string> {
return Q.when(this.content);
}

// Returns a promise for the number of bytes in the remote file.
getSize(): Q.Promise<number> {
return Q.when(this.fileLength);
}

getFromCache(start: number, stop: number): ?ArrayBuffer {
return this.buffer.slice(start, stop + 1);
}

}

module.exports = LocalStringFile;
4 changes: 3 additions & 1 deletion src/main/RemoteFile.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
'use strict';

import Q from 'q';
import AbstractFile from './AbstractFile';

type Chunk = {
start: number;
Expand All @@ -15,13 +16,14 @@ type Chunk = {
}


class RemoteFile {
class RemoteFile extends AbstractFile{
url: string;
fileLength: number;
chunks: Array<Chunk>; // regions of file that have already been loaded.
numNetworkRequests: number; // track this for debugging/testing

constructor(url: string) {
super();
this.url = url;
this.fileLength = -1; // unknown
this.chunks = [];
Expand Down
6 changes: 3 additions & 3 deletions src/main/data/vcf.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
'use strict';

import type ContigInterval from '../ContigInterval';
import type RemoteFile from '../RemoteFile';
import type AbstractFile from '../AbstractFile';
import type Q from 'q';

export type Variant = {
Expand Down Expand Up @@ -146,10 +146,10 @@ class ImmediateVcfFile {


class VcfFile {
remoteFile: RemoteFile;
remoteFile: AbstractFile;
immediate: Q.Promise<ImmediateVcfFile>;

constructor(remoteFile: RemoteFile) {
constructor(remoteFile: AbstractFile) {
this.remoteFile = remoteFile;

this.immediate = this.remoteFile.getAllString().then(txt => {
Expand Down
14 changes: 9 additions & 5 deletions src/main/sources/VcfDataSource.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Q from 'q';

import ContigInterval from '../ContigInterval';
import RemoteFile from '../RemoteFile';
import LocalStringFile from '../LocalStringFile';
import VcfFile from '../data/vcf';

export type VcfDataSource = {
Expand Down Expand Up @@ -91,13 +92,16 @@ function createFromVcfFile(remoteSource: VcfFile): VcfDataSource {
return o;
}

function create(data: {url:string}): VcfDataSource {
function create(data: Object): VcfDataSource {
var url = data.url;
if (!url) {
throw new Error(`Missing URL from track: ${JSON.stringify(data)}`);
var content = data.content;
if (url!==null && url!== undefined) {
return createFromVcfFile(new VcfFile(new RemoteFile(url)));
}

return createFromVcfFile(new VcfFile(new RemoteFile(url)));
if (content!==null && content!== undefined) {
return createFromVcfFile(new VcfFile(new LocalStringFile(content)));
}
throw new Error(`Missing URL from track: ${JSON.stringify(data)}`);
}

module.exports = {
Expand Down
113 changes: 113 additions & 0 deletions src/test/LocalStringFile-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/* @flow */
'use strict';

import {expect} from 'chai';

import LocalStringFile from '../main/LocalStringFile';
import jBinary from 'jbinary';

describe('LocalStringFile', () => {
function bufferToText(buf) {
return new jBinary(buf).read('string');
}

it('should fetch a subset of a file', function() {
var f = new LocalStringFile('0123456789\n');
var promisedData = f.getBytes(4, 5);

return promisedData.then(buf => {
expect(buf.byteLength).to.equal(5);
expect(bufferToText(buf)).to.equal('45678');
});
});

it('should fetch subsets from cache', function() {
var f = new LocalStringFile('0123456789\n');
return f.getBytes(0, 10).then(buf => {
expect(buf.byteLength).to.equal(10);
expect(bufferToText(buf)).to.equal('0123456789');
return f.getBytes(4, 5).then(buf => {
expect(buf.byteLength).to.equal(5);
expect(bufferToText(buf)).to.equal('45678');
});
});
});

it('should fetch entire files', function() {
var f = new LocalStringFile('0123456789\n');
return f.getAll().then(buf => {
expect(buf.byteLength).to.equal(11);
expect(bufferToText(buf)).to.equal('0123456789\n');
});
});

it('should determine file lengths', function() {
var f = new LocalStringFile('0123456789\n');
return f.getSize().then(size => {
expect(size).to.equal(11);
});
});

it('should get file lengths from full requests', function() {
var f = new LocalStringFile('0123456789\n');
return f.getAll().then(buf => {
return f.getSize().then(size => {
expect(size).to.equal(11);
});
});
});

it('should get file lengths from range requests', function() {
var f = new LocalStringFile('0123456789\n');
return f.getBytes(4, 5).then(buf => {
return f.getSize().then(size => {
expect(size).to.equal(11);
});
});
});

it('should cache requests for full files', function() {
var f = new LocalStringFile('0123456789\n');
f.getAll().then(buf => {
expect(buf.byteLength).to.equal(11);
expect(bufferToText(buf)).to.equal('0123456789\n');
return f.getAll().then(buf => {
expect(buf.byteLength).to.equal(11);
expect(bufferToText(buf)).to.equal('0123456789\n');
});
});
});

it('should serve range requests from cache after getAll', function() {
var f = new LocalStringFile('0123456789\n');
return f.getAll().then(buf => {
expect(buf.byteLength).to.equal(11);
expect(bufferToText(buf)).to.equal('0123456789\n');
return f.getBytes(4, 5).then(buf => {
expect(buf.byteLength).to.equal(5);
expect(bufferToText(buf)).to.equal('45678');
});
});
});

it('should truncate requests past EOF', function() {
var f = new LocalStringFile('0123456789\n');
var promisedData = f.getBytes(4, 100);

return promisedData.then(buf => {
expect(buf.byteLength).to.equal(7);
expect(bufferToText(buf)).to.equal('456789\n');
return f.getBytes(6, 90).then(buf => {
expect(buf.byteLength).to.equal(5);
expect(bufferToText(buf)).to.equal('6789\n');
});
});
});

it('should fetch entire files as strings', function() {
var f = new LocalStringFile('0123456789\n');
return f.getAllString().then(txt => {
expect(txt).to.equal('0123456789\n');
});
});
});
56 changes: 56 additions & 0 deletions src/test/data/vcf-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {expect} from 'chai';
import VcfFile from '../../main/data/vcf';
import ContigInterval from '../../main/ContigInterval';
import RemoteFile from '../../main/RemoteFile';
import LocalStringFile from '../../main/LocalStringFile';

describe('VCF', function() {
it('should respond to queries', function() {
Expand Down Expand Up @@ -51,3 +52,58 @@ describe('VCF', function() {
});
});
});
describe('VCF from string content', function() {
var content = '';
before(function() {
content =
"##fileformat=VCFv4.1\n"+
"##source=VarScan2\n"+
"##INFO=<ID=DP,Number=1,Type=Integer,Description=\"Total depth of quality bases\">\n"+
"##INFO=<ID=SOMATIC,Number=0,Type=Flag,Description=\"Indicates if record is a somatic mutation\">\n"+
"##INFO=<ID=SS,Number=1,Type=String,Description=\"Somatic status of variant (0=Reference,1=Germline,2=Somatic,3=LOH, or 5=Unknown)\">\n"+
"##INFO=<ID=SSC,Number=1,Type=String,Description=\"Somatic score in Phred scale (0-255) derived from somatic p-value\">\n"+
"##INFO=<ID=GPV,Number=1,Type=Float,Description=\"Fisher's Exact Test P-value of tumor+normal versus no variant for Germline calls\">\n"+
"##INFO=<ID=SPV,Number=1,Type=Float,Description=\"Fisher's Exact Test P-value of tumor versus normal for Somatic/LOH calls\">\n"+
"##FILTER=<ID=str10,Description=\"Less than 10% or more than 90% of variant supporting reads on one strand\">\n"+
"##FILTER=<ID=indelError,Description=\"Likely artifact due to indel reads at this position\">\n"+
"##FORMAT=<ID=GT,Number=1,Type=String,Description=\"Genotype\">\n"+
"##FORMAT=<ID=GQ,Number=1,Type=Integer,Description=\"Genotype Quality\">\n"+
"##FORMAT=<ID=DP,Number=1,Type=Integer,Description=\"Read Depth\">\n"+
"##FORMAT=<ID=RD,Number=1,Type=Integer,Description=\"Depth of reference-supporting bases (reads1)\">\n"+
"##FORMAT=<ID=AD,Number=1,Type=Integer,Description=\"Depth of variant-supporting bases (reads2)\">\n"+
"##FORMAT=<ID=FREQ,Number=1,Type=String,Description=\"Variant allele frequency\">\n"+
"##FORMAT=<ID=DP4,Number=4,Type=Integer,Description=\"Strand read counts: ref/fwd, ref/rev, var/fwd, var/rev\">\n"+
"#CHROM POS ID REF ALT QUAL FILTER INFO FORMAT NORMAL TUMOR\n"+
"20 61795 . G T . PASS DP=81;SS=1;SSC=2;GPV=4.6768E-16;SPV=5.4057E-1 GT:GQ:DP:RD:AD:FREQ:DP4 0/1:.:44:22:22:50%:16,6,9,13 0/1:.:37:18:19:51.35%:10,8,10,9\n"+
"20 62731 . C A . PASS DP=68;SS=1;SSC=1;GPV=1.4855E-11;SPV=7.5053E-1 GT:GQ:DP:RD:AD:FREQ:DP4 0/1:.:32:17:15:46.88%:9,8,9,6 0/1:.:36:21:15:41.67%:8,13,8,7\n"+
"20 63799 . C T . PASS DP=72;SS=1;SSC=7;GPV=3.6893E-16;SPV=1.8005E-1 GT:GQ:DP:RD:AD:FREQ:DP4 0/1:.:39:19:19:50%:8,11,11,8 0/1:.:33:12:21:63.64%:5,7,8,13\n"+
"20 65288 . G T . PASS DP=35;SS=1;SSC=0;GPV=7.8434E-5;SPV=8.2705E-1 GT:GQ:DP:RD:AD:FREQ:DP4 0/1:.:21:13:8:38.1%:4,9,0,8 0/1:.:14:10:4:28.57%:2,8,0,4\n"+
"20 65900 . G A . PASS DP=53;SS=1;SSC=0;GPV=1.5943E-31;SPV=1E0 GT:GQ:DP:RD:AD:FREQ:DP4 1/1:.:26:0:26:100%:0,0,12,14 1/1:.:27:0:27:100%:0,0,15,12\n"+
"20 66370 . G A . PASS DP=66;SS=1;SSC=0;GPV=2.6498E-39;SPV=1E0 GT:GQ:DP:RD:AD:FREQ:DP4 1/1:.:32:0:32:100%:0,0,11,21 1/1:.:34:0:34:100%:0,0,15,19\n"+
"20 68749 . T C . PASS DP=64;SS=1;SSC=0;GPV=4.1752E-38;SPV=1E0 GT:GQ:DP:RD:AD:FREQ:DP4 1/1:.:23:0:23:100%:0,0,7,16 1/1:.:41:0:41:100%:0,0,21,20\n"+
"20 69094 . G A . PASS DP=25;SS=1;SSC=8;GPV=4.2836E-5;SPV=1.5657E-1 GT:GQ:DP:RD:AD:FREQ:DP4 0/1:.:12:8:4:33.33%:5,3,4,0 0/1:.:13:5:8:61.54%:3,2,6,2\n"+
"20 69408 . C T . PASS DP=53;SS=1;SSC=0;GPV=8.7266E-12;SPV=9.8064E-1 GT:GQ:DP:RD:AD:FREQ:DP4 0/1:.:27:9:18:66.67%:5,4,9,9 0/1:.:26:15:11:42.31%:6,9,4,7\n"+
"20 75254 . C A . PASS DP=74;SS=1;SSC=9;GPV=7.9203E-12;SPV=1.1567E-1 GT:GQ:DP:RD:AD:FREQ:DP4 0/1:.:34:22:11:33.33%:13,9,5,6 0/1:.:40:20:20:50%:5,15,14,6\n";
});

it('should respond to queries', function() {
var vcf = new VcfFile(new LocalStringFile(content));
var range = new ContigInterval('20', 63799, 69094);
return vcf.getFeaturesInRange(range).then(features => {
expect(features).to.have.length(6);

var v0 = features[0],
v5 = features[5];

expect(v0.contig).to.equal('20');
expect(v0.position).to.equal(63799);
expect(v0.ref).to.equal('C');
expect(v0.alt).to.equal('T');

expect(v5.contig).to.equal('20');
expect(v5.position).to.equal(69094);
expect(v5.ref).to.equal('G');
expect(v5.alt).to.equal('A');
});
});
});

0 comments on commit e454971

Please sign in to comment.