diff --git a/Tasks/VSMobileCenterUpload/Tests/L0.ts b/Tasks/VSMobileCenterUpload/Tests/L0.ts index b5955f56a408..0f815002855f 100644 --- a/Tasks/VSMobileCenterUpload/Tests/L0.ts +++ b/Tasks/VSMobileCenterUpload/Tests/L0.ts @@ -15,6 +15,7 @@ describe('VSMobileCenterUpload L0 Suite', function () { process.env["ENDPOINT_AUTH_MyTestEndpoint"] = "{\"parameters\":{\"apitoken\":\"mytoken123\"},\"scheme\":\"apitoken\"}"; process.env["ENDPOINT_URL_MyTestEndpoint"] = "https://example.test/v0.1"; process.env["ENDPOINT_AUTH_PARAMETER_MyTestEndpoint_APITOKEN"] = "mytoken123"; + process.env["SYSTEM_DEFAULTWORKINGDIRECTORY"]="/agent/1/_work"; }); after(() => { @@ -78,6 +79,66 @@ describe('VSMobileCenterUpload L0 Suite', function () { tr.run(); assert(tr.failed, 'task should have failed'); + done() + }); + + it('Positive path: single file with Include Parent', (done: MochaDone) => { + this.timeout(2000); + + let tp = path.join(__dirname, 'L0SymIncludeParent.js'); + let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); + + tr.run(); + assert(tr.succeeded, 'task should have succeeded'); + + done() + }); + + it('Positive path: multiple dSYMs in the same foder', (done: MochaDone) => { + this.timeout(2000); + + let tp = path.join(__dirname, 'L0SymMultipleDSYMs_flat_1.js'); + let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); + + tr.run(); + assert(tr.succeeded, 'task should have succeeded'); + + done() + }); + + it('Positive path: multiple dSYMs in parallel foders', (done: MochaDone) => { + this.timeout(2000); + + let tp = path.join(__dirname, 'L0SymMultipleDSYMs_flat_2.js'); + let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); + + tr.run(); + assert(tr.succeeded, 'task should have succeeded'); + + done() + }); + + it('Positive path: multiple dSYMs in a tree', (done: MochaDone) => { + this.timeout(2000); + + let tp = path.join(__dirname, 'L0SymMultipleDSYMs_tree.js'); + let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); + + tr.run(); + assert(tr.succeeded, 'task should have succeeded'); + + done() + }); + + it('Positive path: a single dSYM', (done: MochaDone) => { + this.timeout(2000); + + let tp = path.join(__dirname, 'L0SymMultipleDSYMs_single.js'); + let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); + + tr.run(); + assert(tr.succeeded, 'task should have succeeded'); + done() }) }); diff --git a/Tasks/VSMobileCenterUpload/Tests/L0ApiRejectsFail.ts b/Tasks/VSMobileCenterUpload/Tests/L0ApiRejectsFail.ts index 49e68edada21..f780f9c55b68 100644 --- a/Tasks/VSMobileCenterUpload/Tests/L0ApiRejectsFail.ts +++ b/Tasks/VSMobileCenterUpload/Tests/L0ApiRejectsFail.ts @@ -25,18 +25,14 @@ nock('https://example.test') let a: ma.TaskLibAnswers = { "checkPath" : { "/test/path/to/my.ipa": true + }, + "glob" : { + "/test/path/to/my.ipa": [ + "/test/path/to/my.ipa" + ] } }; tmr.setAnswers(a); -tmr.registerMock('./utils.js', { - resolveSinglePath: function(s) { - return s ? s : null; - }, - checkAndFixFilePath: function(p, name) { - return p; - } -}); - tmr.run(); diff --git a/Tasks/VSMobileCenterUpload/Tests/L0MultipleIpaFail.ts b/Tasks/VSMobileCenterUpload/Tests/L0MultipleIpaFail.ts index 39c6e063ee6b..42a6c9a3823a 100644 --- a/Tasks/VSMobileCenterUpload/Tests/L0MultipleIpaFail.ts +++ b/Tasks/VSMobileCenterUpload/Tests/L0MultipleIpaFail.ts @@ -21,12 +21,18 @@ let a: ma.TaskLibAnswers = { "checkPath" : { "/test/path/to/one.ipa": true, "/test/path/to/two.ipa": true + }, + "glob" : { + "/test/path/to/*.ipa": [ + "/test/path/to/one.ipa", + "/test/path/to/two.ipa" + ] } }; tmr.setAnswers(a); tmr.registerMock('./utils.js', { - resolveSinglePath: function(s) { + resolveSinglePath: function(s, b1, b2) { throw new Error("Matched multiple files"); }, checkAndFixFilePath: function(p, name) { diff --git a/Tasks/VSMobileCenterUpload/Tests/L0OneIpaPass.ts b/Tasks/VSMobileCenterUpload/Tests/L0OneIpaPass.ts index 742e31a5881b..3c8bcedef911 100644 --- a/Tasks/VSMobileCenterUpload/Tests/L0OneIpaPass.ts +++ b/Tasks/VSMobileCenterUpload/Tests/L0OneIpaPass.ts @@ -80,21 +80,21 @@ nock('https://example.test') // provide answers for task mock let a: ma.TaskLibAnswers = { "checkPath" : { - "/test/path/to/my.ipa": true + "/test/path/to/my.ipa": true, + "/test/path/to/mappings.txt": true + }, + "glob" : { + "/test/path/to/mappings.txt": [ + "/test/path/to/mappings.txt" + ], + "/test/path/to/my.ipa": [ + "/test/path/to/my.ipa" + ] } }; tmr.setAnswers(a); -tmr.registerMock('./utils.js', { - resolveSinglePath: function(s) { - return s ? s : null; - }, - checkAndFixFilePath: function(p, name) { - return p; - } -}); - -fs.createReadStream = (s) => { +fs.createReadStream = (s: string) => { let stream = new Readable; stream.push(s); stream.push(null); @@ -102,11 +102,16 @@ fs.createReadStream = (s) => { return stream; }; -fs.statSync = (s) => { +fs.statSync = (s: string) => { let stat = new Stats; + stat.isFile = () => { - return true; + return !s.toLowerCase().endsWith(".dsym"); + } + stat.isDirectory = () => { + return s.toLowerCase().endsWith(".dsym"); } + stat.size = 100; return stat; } diff --git a/Tasks/VSMobileCenterUpload/Tests/L0SymIncludeParent.ts b/Tasks/VSMobileCenterUpload/Tests/L0SymIncludeParent.ts new file mode 100644 index 000000000000..349933fc5b0e --- /dev/null +++ b/Tasks/VSMobileCenterUpload/Tests/L0SymIncludeParent.ts @@ -0,0 +1,172 @@ + +import ma = require('vsts-task-lib/mock-answer'); +import tmrm = require('vsts-task-lib/mock-run'); +import path = require('path'); +import fs = require('fs'); +var Readable = require('stream').Readable +var Writable = require('stream').Writable +var Stats = require('fs').Stats + +var nock = require('nock'); + +let taskPath = path.join(__dirname, '..', 'vsmobilecenterupload.js'); +let tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath); + +tmr.setInput('serverEndpoint', 'MyTestEndpoint'); +tmr.setInput('appSlug', 'testuser/testapp'); +tmr.setInput('app', '/test/path/to/my.ipa'); +tmr.setInput('releaseNotesSelection', 'releaseNotesInput'); +tmr.setInput('releaseNotesInput', 'my release notes'); +tmr.setInput('symbolsType', 'AndroidJava'); +tmr.setInput('mappingTxtPath', '/test/path/to/mappings.txt'); +tmr.setInput('packParentFolder', 'true'); + +//prepare upload +nock('https://example.test') + .post('/v0.1/apps/testuser/testapp/release_uploads') + .reply(201, { + upload_id: 1, + upload_url: 'https://example.upload.test/release_upload' + }); + +//upload +nock('https://example.upload.test') + .post('/release_upload') + .reply(201, { + status: 'success' + }); + +//finishing upload, commit the package +nock('https://example.test') + .patch('/v0.1/apps/testuser/testapp/release_uploads/1', { + status: 'committed' + }) + .reply(200, { + release_url: 'my_release_location' + }); + +//make it available +nock('https://example.test') + .patch('/my_release_location', { + status: 'available', + distribution_group_id:'00000000-0000-0000-0000-000000000000', + release_notes:'my release notes' + }) + .reply(200); + +//begin symbol upload +nock('https://example.test') + .post('/v0.1/apps/testuser/testapp/symbol_uploads', { + symbol_type: 'AndroidJava' + }) + .reply(201, { + symbol_upload_id: 100, + upload_url: 'https://example.upload.test/symbol_upload', + expiration_date: 1234567 + }); + +//upload symbols +nock('https://example.upload.test') + .put('/symbol_upload') + .reply(201, { + status: 'success' + }); + +//finishing symbol upload, commit the symbol +nock('https://example.test') + .patch('/v0.1/apps/testuser/testapp/symbol_uploads/100', { + status: 'committed' + }) + .reply(200); + +// provide answers for task mock +let a: ma.TaskLibAnswers = { + 'checkPath' : { + '/test/path/to/my.ipa': true, + '/test/path/to/mappings.txt': true, + '/test/path/to': true, + '/test/path/to/f1.txt': true, + '/test/path/to/f2.txt': true, + '/test/path/to/folder': true, + '/test/path/to/folder/f11.txt': true, + '/test/path/to/folder/f12.txt': true + }, + 'glob' : { + '/test/path/to/mappings.txt': [ + '/test/path/to/mappings.txt' + ], + '/test/path/to/my.ipa': [ + '/test/path/to/my.ipa' + ] + } +}; +tmr.setAnswers(a); + +fs.createReadStream = (s: string) => { + let stream = new Readable; + stream.push(s); + stream.push(null); + + return stream; +}; + +fs.createWriteStream = (s: string) => { + let stream = new Writable; + + stream.write = () => {}; + + return stream; +}; + +fs.readdirSync = (folder: string) => { + let files: string[] = []; + + if (folder === '/test/path/to') { + files = [ + 'mappings.txt', + 'f1.txt', + 'f2.txt', + 'folder' + ] + } else if (folder === '/test/path/to/folder') { + files = [ + 'f11.txt', + 'f12.txt' + ] + } + + return files; +}; + +fs.statSync = (s: string) => { + let stat = new Stats; +// s = s.replace("\\", "/"); + + stat.isFile = () => { + if (s === '/test/path/to') { + return false; + } else if (s === '/test/path/to/folder') { + return false; + } else { + return true; + } + } + + stat.isDirectory = () => { + if (s === '/test/path/to') { + return true; + } else if (s === '/test/path/to/folder') { + return true; + } else { + return false; + } + } + + stat.size = 100; + + return stat; +} +tmr.registerMock('fs', fs); + +tmr.run(); + diff --git a/Tasks/VSMobileCenterUpload/Tests/L0SymMultipleDSYMs_flat_1.ts b/Tasks/VSMobileCenterUpload/Tests/L0SymMultipleDSYMs_flat_1.ts new file mode 100644 index 000000000000..425f861aeca8 --- /dev/null +++ b/Tasks/VSMobileCenterUpload/Tests/L0SymMultipleDSYMs_flat_1.ts @@ -0,0 +1,208 @@ + +import ma = require('vsts-task-lib/mock-answer'); +import tmrm = require('vsts-task-lib/mock-run'); +import path = require('path'); +import fs = require('fs'); +var Readable = require('stream').Readable +var Writable = require('stream').Writable +var Stats = require('fs').Stats + +var nock = require('nock'); + +let taskPath = path.join(__dirname, '..', 'vsmobilecenterupload.js'); +let tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath); + +tmr.setInput('serverEndpoint', 'MyTestEndpoint'); +tmr.setInput('appSlug', 'testuser/testapp'); +tmr.setInput('app', '/test/path/to/my.ipa'); +tmr.setInput('releaseNotesSelection', 'releaseNotesInput'); +tmr.setInput('releaseNotesInput', 'my release notes'); +tmr.setInput('symbolsType', 'Apple'); +tmr.setInput('dsymPath', 'a/b/c/(x|y).dsym'); + +/* + dSyms folder structure: + a + f.txt + b + f.txt + c + d + f.txt + f.txt + x.dsym + x1.txt + x2.txt + y.dsym + y1.txt +*/ + +//prepare upload +nock('https://example.test') + .post('/v0.1/apps/testuser/testapp/release_uploads') + .reply(201, { + upload_id: 1, + upload_url: 'https://example.upload.test/release_upload' + }); + +//upload +nock('https://example.upload.test') + .post('/release_upload') + .reply(201, { + status: 'success' + }); + +//finishing upload, commit the package +nock('https://example.test') + .patch('/v0.1/apps/testuser/testapp/release_uploads/1', { + status: 'committed' + }) + .reply(200, { + release_url: 'my_release_location' + }); + +//make it available +nock('https://example.test') + .patch('/my_release_location', { + status: 'available', + distribution_group_id:'00000000-0000-0000-0000-000000000000', + release_notes:'my release notes' + }) + .reply(200); + +//begin symbol upload +nock('https://example.test') + .post('/v0.1/apps/testuser/testapp/symbol_uploads', { + symbol_type: 'Apple' + }) + .reply(201, { + symbol_upload_id: 100, + upload_url: 'https://example.upload.test/symbol_upload', + expiration_date: 1234567 + }); + +//upload symbols +nock('https://example.upload.test') + .put('/symbol_upload') + .reply(201, { + status: 'success' + }); + +//finishing symbol upload, commit the symbol +nock('https://example.test') + .patch('/v0.1/apps/testuser/testapp/symbol_uploads/100', { + status: 'committed' + }) + .reply(200); + +// provide answers for task mock +let a: ma.TaskLibAnswers = { + 'checkPath' : { + '/test/path/to/my.ipa': true, + 'a': true, + 'a/f.txt': true, + 'a/b': true, + 'a/b/f.txt': true, + 'a/b/c': true, + 'a/b/c/f.txt': true, + 'a/b/c/d': true, + 'a/b/c/d/f.txt': true, + 'a/b/c/x.dsym': true, + 'a/b/c/x.dsym/x1.txt': true, + 'a/b/c/x.dsym/x2.txt': true, + 'a/b/c/y.dsym': true, + 'a/b/c/y.dsym/y1.txt': true + }, + 'glob' : { + 'a/b/c/(x|y).dsym': [ + 'a/b/c/x.dsym', + 'a/b/c/y.dsym' + ], + '/test/path/to/my.ipa': [ + '/test/path/to/my.ipa' + ] + } +}; +tmr.setAnswers(a); + +fs.createReadStream = (s: string) => { + let stream = new Readable; + stream.push(s); + stream.push(null); + + return stream; +}; + +fs.createWriteStream = (s: string) => { + let stream = new Writable; + + stream.write = () => {}; + + return stream; +}; + +fs.readdirSync = (folder: string) => { + let files: string[] = []; + + if (folder === 'a') { + files = [ + 'f.txt', + 'b' + ] + } else if (folder === 'a/b') { + files = [ + 'f.txt', + 'c' + ] + } else if (folder === 'a/b/c') { + files = [ + 'f.txt', + 'd', + 'x.dsym', + 'y.dsym' + ] + } else if (folder === 'a/b/c/d') { + files = [ + 'f.txt' + ] + } else if (folder === 'a/b/c/x.dsym') { + files = [ + 'x1.txt', + 'x2.txt' + ] + } else if (folder === 'a/b/c/y.dsym') { + files = [ + 'y1.txt' + ] + } + + return files; +}; + +fs.statSync = (s: string) => { + let stat = new Stats; + + stat.isFile = () => { + if (s.endsWith('.txt')) { + return true; + } else { + return false; + } + } + + stat.isDirectory = () => { + if (s.endsWith('.txt')) { + return false; + } else { + return true; + } + } + + stat.size = 100; + + return stat; +} +tmr.registerMock('fs', fs); + +tmr.run(); + diff --git a/Tasks/VSMobileCenterUpload/Tests/L0SymMultipleDSYMs_flat_2.ts b/Tasks/VSMobileCenterUpload/Tests/L0SymMultipleDSYMs_flat_2.ts new file mode 100644 index 000000000000..7d7231dcea05 --- /dev/null +++ b/Tasks/VSMobileCenterUpload/Tests/L0SymMultipleDSYMs_flat_2.ts @@ -0,0 +1,218 @@ + +import ma = require('vsts-task-lib/mock-answer'); +import tmrm = require('vsts-task-lib/mock-run'); +import path = require('path'); +import fs = require('fs'); +var Readable = require('stream').Readable +var Writable = require('stream').Writable +var Stats = require('fs').Stats + +var nock = require('nock'); + +let taskPath = path.join(__dirname, '..', 'vsmobilecenterupload.js'); +let tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath); + +tmr.setInput('serverEndpoint', 'MyTestEndpoint'); +tmr.setInput('appSlug', 'testuser/testapp'); +tmr.setInput('app', '/test/path/to/my.ipa'); +tmr.setInput('releaseNotesSelection', 'releaseNotesInput'); +tmr.setInput('releaseNotesInput', 'my release notes'); +tmr.setInput('symbolsType', 'Apple'); +tmr.setInput('dsymPath', 'a/**/(x|y).dsym'); + +/* + dSyms folder structure: + a + f.txt + b + f.txt + c + d + f.txt + f.txt + x.dsym + x1.txt + x2.txt + d + f.txt + y.dsym + y1.txt +*/ + +//prepare upload +nock('https://example.test') + .post('/v0.1/apps/testuser/testapp/release_uploads') + .reply(201, { + upload_id: 1, + upload_url: 'https://example.upload.test/release_upload' + }); + +//upload +nock('https://example.upload.test') + .post('/release_upload') + .reply(201, { + status: 'success' + }); + +//finishing upload, commit the package +nock('https://example.test') + .patch('/v0.1/apps/testuser/testapp/release_uploads/1', { + status: 'committed' + }) + .reply(200, { + release_url: 'my_release_location' + }); + +//make it available +nock('https://example.test') + .patch('/my_release_location', { + status: 'available', + distribution_group_id:'00000000-0000-0000-0000-000000000000', + release_notes:'my release notes' + }) + .reply(200); + +//begin symbol upload +nock('https://example.test') + .post('/v0.1/apps/testuser/testapp/symbol_uploads', { + symbol_type: 'Apple' + }) + .reply(201, { + symbol_upload_id: 100, + upload_url: 'https://example.upload.test/symbol_upload', + expiration_date: 1234567 + }); + +//upload symbols +nock('https://example.upload.test') + .put('/symbol_upload') + .reply(201, { + status: 'success' + }); + +//finishing symbol upload, commit the symbol +nock('https://example.test') + .patch('/v0.1/apps/testuser/testapp/symbol_uploads/100', { + status: 'committed' + }) + .reply(200); + +// provide answers for task mock +let a: ma.TaskLibAnswers = { + 'checkPath' : { + '/test/path/to/my.ipa': true, + 'a': true, + 'a/f.txt': true, + 'a/b': true, + 'a/b/f.txt': true, + 'a/b/c': true, + 'a/b/c/f.txt': true, + 'a/b/c/d': true, + 'a/b/c/d/f.txt': true, + 'a/b/c/x.dsym': true, + 'a/b/c/x.dsym/x1.txt': true, + 'a/b/c/x.dsym/x2.txt': true, + 'a/b/d': true, + 'a/b/d/f.txt': true, + 'a/b/d/y.dsym': true, + 'a/b/d/y.dsym/y1.txt': true + }, + 'glob' : { + 'a/**/(x|y).dsym': [ + 'a/b/c/x.dsym', + 'a/b/d/y.dsym' + ], + '/test/path/to/my.ipa': [ + '/test/path/to/my.ipa' + ] + } +}; +tmr.setAnswers(a); + +fs.createReadStream = (s: string) => { + let stream = new Readable; + stream.push(s); + stream.push(null); + + return stream; +}; + +fs.createWriteStream = (s: string) => { + let stream = new Writable; + + stream.write = () => {}; + + return stream; +}; + +fs.readdirSync = (folder: string) => { + let files: string[] = []; + + if (folder === 'a') { + files = [ + 'f.txt', + 'b' + ] + } else if (folder === 'a/b') { + files = [ + 'f.txt', + 'c', + 'd' + ] + } else if (folder === 'a/b/c') { + files = [ + 'f.txt', + 'd', + 'x.dsym', + 'y.dsym' + ] + } else if (folder === 'a/b/c/d') { + files = [ + 'f.txt' + ] + } else if (folder === 'a/b/c/x.dsym') { + files = [ + 'x1.txt', + 'x2.txt' + ] + } else if (folder === 'a/b/d') { + files = [ + 'f.txt', + 'y.dsym' + ] + } else if (folder === 'a/b/d/y.dsym') { + files = [ + 'y1.txt' + ] + } + + return files; +}; + +fs.statSync = (s: string) => { + let stat = new Stats; + + stat.isFile = () => { + if (s.endsWith('.txt')) { + return true; + } else { + return false; + } + } + + stat.isDirectory = () => { + if (s.endsWith('.txt')) { + return false; + } else { + return true; + } + } + + stat.size = 100; + + return stat; +} +tmr.registerMock('fs', fs); + +tmr.run(); + diff --git a/Tasks/VSMobileCenterUpload/Tests/L0SymMultipleDSYMs_single.ts b/Tasks/VSMobileCenterUpload/Tests/L0SymMultipleDSYMs_single.ts new file mode 100644 index 000000000000..036dca50ffc3 --- /dev/null +++ b/Tasks/VSMobileCenterUpload/Tests/L0SymMultipleDSYMs_single.ts @@ -0,0 +1,199 @@ + +import ma = require('vsts-task-lib/mock-answer'); +import tmrm = require('vsts-task-lib/mock-run'); +import path = require('path'); +import fs = require('fs'); +var Readable = require('stream').Readable +var Writable = require('stream').Writable +var Stats = require('fs').Stats + +var nock = require('nock'); + +let taskPath = path.join(__dirname, '..', 'vsmobilecenterupload.js'); +let tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath); + +tmr.setInput('serverEndpoint', 'MyTestEndpoint'); +tmr.setInput('appSlug', 'testuser/testapp'); +tmr.setInput('app', '/test/path/to/my.ipa'); +tmr.setInput('releaseNotesSelection', 'releaseNotesInput'); +tmr.setInput('releaseNotesInput', 'my release notes'); +tmr.setInput('symbolsType', 'Apple'); +tmr.setInput('dsymPath', 'a/**/*.dsym'); + +/* + dSyms folder structure: + a + f.txt + b + f.txt + c + d + f.txt + f.txt + x.dsym + x1.txt + x2.txt +*/ + +//prepare upload +nock('https://example.test') + .post('/v0.1/apps/testuser/testapp/release_uploads') + .reply(201, { + upload_id: 1, + upload_url: 'https://example.upload.test/release_upload' + }); + +//upload +nock('https://example.upload.test') + .post('/release_upload') + .reply(201, { + status: 'success' + }); + +//finishing upload, commit the package +nock('https://example.test') + .patch('/v0.1/apps/testuser/testapp/release_uploads/1', { + status: 'committed' + }) + .reply(200, { + release_url: 'my_release_location' + }); + +//make it available +nock('https://example.test') + .patch('/my_release_location', { + status: 'available', + distribution_group_id:'00000000-0000-0000-0000-000000000000', + release_notes:'my release notes' + }) + .reply(200); + +//begin symbol upload +nock('https://example.test') + .post('/v0.1/apps/testuser/testapp/symbol_uploads', { + symbol_type: 'Apple' + }) + .reply(201, { + symbol_upload_id: 100, + upload_url: 'https://example.upload.test/symbol_upload', + expiration_date: 1234567 + }); + +//upload symbols +nock('https://example.upload.test') + .put('/symbol_upload') + .reply(201, { + status: 'success' + }); + +//finishing symbol upload, commit the symbol +nock('https://example.test') + .patch('/v0.1/apps/testuser/testapp/symbol_uploads/100', { + status: 'committed' + }) + .reply(200); + +// provide answers for task mock +let a: ma.TaskLibAnswers = { + 'checkPath' : { + '/test/path/to/my.ipa': true, + 'a': true, + 'a/f.txt': true, + 'a/b': true, + 'a/b/f.txt': true, + 'a/b/c': true, + 'a/b/c/f.txt': true, + 'a/b/c/d': true, + 'a/b/c/d/f.txt': true, + 'a/b/c/x.dsym': true, + 'a/b/c/x.dsym/x1.txt': true, + 'a/b/c/x.dsym/x2.txt': true + }, + 'glob' : { + 'a/**/*.dsym': [ + 'a/b/c/x.dsym' + ], + '/test/path/to/my.ipa': [ + '/test/path/to/my.ipa' + ] + } +}; +tmr.setAnswers(a); + +fs.createReadStream = (s: string) => { + let stream = new Readable; + stream.push(s); + stream.push(null); + + return stream; +}; + +fs.createWriteStream = (s: string) => { + let stream = new Writable; + + stream.write = () => {}; + + return stream; +}; + +fs.readdirSync = (folder: string) => { + let files: string[] = []; + + if (folder === 'a') { + files = [ + 'f.txt', + 'b' + ] + } else if (folder === 'a/b') { + files = [ + 'f.txt', + 'c', + 'd' + ] + } else if (folder === 'a/b/c') { + files = [ + 'f.txt', + 'd', + 'x.dsym' + ] + } else if (folder === 'a/b/c/x.dsym') { + files = [ + 'x1.txt', + 'x2.txt' + ] + } else if (folder === 'a/b/c/d') { + files = [ + 'f.txt' + ] + } + + return files; +}; + +fs.statSync = (s: string) => { + let stat = new Stats; + + stat.isFile = () => { + if (s.endsWith('.txt')) { + return true; + } else { + return false; + } + } + + stat.isDirectory = () => { + if (s.endsWith('.txt')) { + return false; + } else { + return true; + } + } + + stat.size = 100; + + return stat; +} +tmr.registerMock('fs', fs); + +tmr.run(); + diff --git a/Tasks/VSMobileCenterUpload/Tests/L0SymMultipleDSYMs_tree.ts b/Tasks/VSMobileCenterUpload/Tests/L0SymMultipleDSYMs_tree.ts new file mode 100644 index 000000000000..c3c669ca2b55 --- /dev/null +++ b/Tasks/VSMobileCenterUpload/Tests/L0SymMultipleDSYMs_tree.ts @@ -0,0 +1,232 @@ + +import ma = require('vsts-task-lib/mock-answer'); +import tmrm = require('vsts-task-lib/mock-run'); +import path = require('path'); +import fs = require('fs'); +var Readable = require('stream').Readable +var Writable = require('stream').Writable +var Stats = require('fs').Stats + +var nock = require('nock'); + +let taskPath = path.join(__dirname, '..', 'vsmobilecenterupload.js'); +let tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath); + +tmr.setInput('serverEndpoint', 'MyTestEndpoint'); +tmr.setInput('appSlug', 'testuser/testapp'); +tmr.setInput('app', '/test/path/to/my.ipa'); +tmr.setInput('releaseNotesSelection', 'releaseNotesInput'); +tmr.setInput('releaseNotesInput', 'my release notes'); +tmr.setInput('symbolsType', 'Apple'); +tmr.setInput('dsymPath', 'a/**/(x|y).dsym'); + +/* + dSyms folder structure: + a + f.txt + b + f.txt + c + d + f.txt + f.txt + x.dsym + x1.txt + x2.txt + d + f.txt + e + f.txt + f + f.txt + y.dsym + y1.txt +*/ + +//prepare upload +nock('https://example.test') + .post('/v0.1/apps/testuser/testapp/release_uploads') + .reply(201, { + upload_id: 1, + upload_url: 'https://example.upload.test/release_upload' + }); + +//upload +nock('https://example.upload.test') + .post('/release_upload') + .reply(201, { + status: 'success' + }); + +//finishing upload, commit the package +nock('https://example.test') + .patch('/v0.1/apps/testuser/testapp/release_uploads/1', { + status: 'committed' + }) + .reply(200, { + release_url: 'my_release_location' + }); + +//make it available +nock('https://example.test') + .patch('/my_release_location', { + status: 'available', + distribution_group_id:'00000000-0000-0000-0000-000000000000', + release_notes:'my release notes' + }) + .reply(200); + +//begin symbol upload +nock('https://example.test') + .post('/v0.1/apps/testuser/testapp/symbol_uploads', { + symbol_type: 'Apple' + }) + .reply(201, { + symbol_upload_id: 100, + upload_url: 'https://example.upload.test/symbol_upload', + expiration_date: 1234567 + }); + +//upload symbols +nock('https://example.upload.test') + .put('/symbol_upload') + .reply(201, { + status: 'success' + }); + +//finishing symbol upload, commit the symbol +nock('https://example.test') + .patch('/v0.1/apps/testuser/testapp/symbol_uploads/100', { + status: 'committed' + }) + .reply(200); + +// provide answers for task mock +let a: ma.TaskLibAnswers = { + 'checkPath' : { + '/test/path/to/my.ipa': true, + 'a': true, + 'a/f.txt': true, + 'a/b': true, + 'a/b/f.txt': true, + 'a/b/c': true, + 'a/b/c/f.txt': true, + 'a/b/c/d': true, + 'a/b/c/d/f.txt': true, + 'a/b/c/x.dsym': true, + 'a/b/c/x.dsym/x1.txt': true, + 'a/b/c/x.dsym/x2.txt': true, + 'a/b/d/f.txt': true, + 'a/b/d': true, + 'a/b/d/e': true, + 'a/b/d/e/f.txt': true, + 'a/b/d/e/f': true, + 'a/b/d/e/f/f.txt': true, + 'a/b/d/e/f/y.dsym': true, + 'a/b/d/e/f/y.dsym/y1.txt': true + }, + 'glob' : { + 'a/**/(x|y).dsym': [ + 'a/b/c/x.dsym', + 'a/b/d/e/f/y.dsym' + ], + '/test/path/to/my.ipa': [ + '/test/path/to/my.ipa' + ] + } +}; +tmr.setAnswers(a); + +fs.createReadStream = (s: string) => { + let stream = new Readable; + stream.push(s); + stream.push(null); + + return stream; +}; + +fs.createWriteStream = (s: string) => { + let stream = new Writable; + + stream.write = () => {}; + + return stream; +}; + +fs.readdirSync = (folder: string) => { + let files: string[] = []; + + if (folder === 'a') { + files = [ + 'f.txt', + 'b' + ] + } else if (folder === 'a/b') { + files = [ + 'f.txt', + 'c', + 'd' + ] + } else if (folder === 'a/b/c') { + files = [ + 'f.txt', + 'd', + 'x.dsym', + 'y.dsym' + ] + } else if (folder === 'a/b/c/x.dsym') { + files = [ + 'x1.txt', + 'x2.txt' + ] + } else if (folder === 'a/b/c/d') { + files = [ + 'f.txt', + 'e' + ] + } else if (folder === 'a/b/c/d/e') { + files = [ + 'f.txt', + 'f' + ] + } else if (folder === 'a/b/d/e/f') { + files = [ + 'f.txt', + 'y.dsym' + ] + } else if (folder === 'a/b/d/e/f/y.dsym') { + files = [ + 'y1.txt' + ] + } + + return files; +}; + +fs.statSync = (s: string) => { + let stat = new Stats; + + stat.isFile = () => { + if (s.endsWith('.txt')) { + return true; + } else { + return false; + } + } + + stat.isDirectory = () => { + if (s.endsWith('.txt')) { + return false; + } else { + return true; + } + } + + stat.size = 100; + + return stat; +} +tmr.registerMock('fs', fs); + +tmr.run(); + diff --git a/Tasks/VSMobileCenterUpload/task.json b/Tasks/VSMobileCenterUpload/task.json index 9ca6e2ca20b1..2f982785f3cd 100644 --- a/Tasks/VSMobileCenterUpload/task.json +++ b/Tasks/VSMobileCenterUpload/task.json @@ -12,7 +12,7 @@ "author": "Microsoft Corporation", "version": { "Major": 0, - "Minor": 113, + "Minor": 114, "Patch": 0 }, "groups": [ diff --git a/Tasks/VSMobileCenterUpload/task.loc.json b/Tasks/VSMobileCenterUpload/task.loc.json index ec55bc2397f9..d41519d06d67 100644 --- a/Tasks/VSMobileCenterUpload/task.loc.json +++ b/Tasks/VSMobileCenterUpload/task.loc.json @@ -12,7 +12,7 @@ "author": "Microsoft Corporation", "version": { "Major": 0, - "Minor": 113, + "Minor": 114, "Patch": 0 }, "groups": [ diff --git a/Tasks/VSMobileCenterUpload/utils.ts b/Tasks/VSMobileCenterUpload/utils.ts index 0eab8996a3b7..822437126baf 100644 --- a/Tasks/VSMobileCenterUpload/utils.ts +++ b/Tasks/VSMobileCenterUpload/utils.ts @@ -5,7 +5,7 @@ import Q = require('q'); var Zip = require('jszip'); -export function checkAndFixFilePath(p, name, continueOnError) { +export function checkAndFixFilePath(p, name, continueOnError): string { if (p) { var workDir = tl.getVariable("System.DefaultWorkingDirectory"); if (arePathEqual(p, workDir)) { @@ -27,24 +27,24 @@ export function checkAndFixFilePath(p, name, continueOnError) { return p; } -function arePathEqual(p1, p2) { +function arePathEqual(p1, p2): boolean { if (!p1 && !p2) return true; else if (!p1 || !p2) return false; else return path.normalize(p1 || "") === path.normalize(p2 || ""); } -function getAllFiles(rootPath, recursive) { - var files = []; +function getAllFiles(rootPath, recursive): string[] { + let files: string[] = []; - var folders = []; + let folders: string[] = []; folders.push(rootPath); while (folders.length > 0) { - var folderPath = folders.shift(); + let folderPath = folders.shift(); - var children = fs.readdirSync(folderPath); - for (var i = 0; i < children.length; i++) { - var childPath = path.join(folderPath, children[i]); + let children = fs.readdirSync(folderPath); + for (let i = 0; i < children.length; i++) { + let childPath = [folderPath, children[i]].join( "/"); if (fs.statSync(childPath).isDirectory()) { if (recursive) { folders.push(childPath); @@ -58,15 +58,32 @@ function getAllFiles(rootPath, recursive) { return files; } -export function createZipStream(rootPath: string, includeFolder: boolean): NodeJS.ReadableStream { +export function createZipStream(symbolsPaths: string[], symbolsRoot: string): NodeJS.ReadableStream { + tl.debug("---- Creating Zip stream"); let zip = new Zip(); - let filePaths = getAllFiles(rootPath, /*recursive=*/ true); - for (let i = 0; i < filePaths.length; i++) { - let filePath = filePaths[i]; - let parentFolder = path.dirname(rootPath); - let relativePath = includeFolder ? path.relative(parentFolder, filePath) : path.relative(rootPath, filePath); - zip.file(relativePath, fs.createReadStream(filePath), { compression: 'DEFLATE' }); - } + + symbolsPaths.forEach(rootPath => { + let filePaths = getAllFiles(rootPath, /*recursive=*/ true); + tl.debug(`------ Adding files: ${filePaths}`); + + for (let i = 0; i < filePaths.length; i++) { + let filePath = filePaths[i]; + + let relativePath: string = null; + if (symbolsRoot) { + relativePath = path.relative(symbolsRoot, filePath); + } else { + // If symbol paths do not have anything common, + // e.g "/a/b/c" and "/x/y/z", or "C:/u/v/w" and "D:/u/v/w", + // let's use "a/b/c" and "x/y/z", or "C/u/v/w" and "D/u/v/w" + // as relative paths in the archive. + relativePath = filePath.replace(/^\/+/,"").replace(":", ""); + } + + tl.debug(`------ Adding zip-entry: ${relativePath} ...`); + zip.file(relativePath, fs.createReadStream(filePath), { compression: 'DEFLATE' }); + } + }); let currentFile = null; let zipStream = zip.generateNodeStream({ @@ -98,7 +115,7 @@ export function createZipFile(zipStream: NodeJS.ReadableStream, filename: string return defer.promise; } -export function isDsym(s: string) { +export function isDsym(s: string): boolean { return (s && s.toLowerCase().endsWith(".dsym")); } @@ -106,9 +123,32 @@ export function removeNewLine(str: string): string { return str.replace(/(\r\n|\n|\r)/gm, ""); } -export function resolveSinglePath(pattern: string, continueOnError: boolean): string { +export function resolveSinglePath(pattern: string, continueOnError: boolean = false, packParentFolder: boolean = false): string { + tl.debug("---- Resolving a single path"); + + let matches = resolvePaths(pattern, continueOnError, packParentFolder); + + if (matches && matches.length > 0) { + if (matches.length != 1) { + if (continueOnError) { + tl.warning(tl.loc("FoundMultipleFiles", pattern)); + } else { + throw new Error(tl.loc("FoundMultipleFiles", pattern)); + } + } + + return matches[0]; + } + + return null; +} + +export function resolvePaths(pattern: string, continueOnError: boolean = false, packParentFolder: boolean = false): string[] { + tl.debug("------ Resolving multiple paths"); + tl.debug(" path pattern: " + (pattern || "")); + if (pattern) { - let matches: string[] = tl.glob(pattern); + let matches = tl.glob(pattern); if (!matches || matches.length === 0) { if (continueOnError) { @@ -119,16 +159,77 @@ export function resolveSinglePath(pattern: string, continueOnError: boolean): st } } - if (matches.length != 1) { - if (continueOnError) { - tl.warning(tl.loc("FoundMultipleFiles", pattern)); - } else { - throw new Error(tl.loc("FoundMultipleFiles", pattern)); - } - } + let selectedPaths = matches.map(v => packParentFolder ? path.dirname(v) : v); + tl.debug(" selectedPaths: " + selectedPaths); - return matches[0]; + let uniquePaths = removeDuplicates(selectedPaths); + tl.debug(" uniquePaths: " + uniquePaths); + + return uniquePaths; } return null; -} \ No newline at end of file +} + +export function removeDuplicates(list: string[]): string[] { + + interface IStringDictionary { [name: string]: number }; + let unique: IStringDictionary = {}; + + list.forEach(s => { + unique[s] = 0; + }); + + return Object.keys(unique); +} + +export function findCommonParent(list: string[]): string { + tl.debug("-- Detecting the common parent of all symbols paths to define the archive's internal folder structure.") + + function cutTail(list: string[], n: number) { + while (n-- > 0) { + list.pop(); + } + } + + if (!list) { + return null; + } + + let commonSegments: string[] = []; + let parentPath: string = null; + + list.forEach((nextPath, idx) => { + tl.debug(`---- next path[${idx}]\t ${nextPath}`); + + if (idx === 0) { + // Take the first path as the common parent candidate + commonSegments = nextPath.split("/"); + } else if (commonSegments.length === 0) { + // We've already detected that the paths do not have a common parent. + // No sense to check the rest of paths. + return null; + } else { + let pathSegmants: string[] = nextPath.split("/"); + + // If the current path contains less segments than the common path calculated so far, + // the trailing segmants in the latter cannot be a part of the resulting common path. + cutTail(commonSegments, commonSegments.length - pathSegmants.length); + + for (let i = 0; i < pathSegmants.length; i++) { + if (pathSegmants[i] !== commonSegments[i]) { + // Segments i, i+1, etc. cannot be a part of the resulting common path. + cutTail(commonSegments, commonSegments.length - i); + break; + } + } + } + + parentPath = commonSegments.join("/"); + tl.debug(` parent path \t ${parentPath}`); + }) + + return parentPath; +} + + diff --git a/Tasks/VSMobileCenterUpload/vsmobilecenterupload.ts b/Tasks/VSMobileCenterUpload/vsmobilecenterupload.ts index cf67383d60c9..1eb16f9debe1 100644 --- a/Tasks/VSMobileCenterUpload/vsmobilecenterupload.ts +++ b/Tasks/VSMobileCenterUpload/vsmobilecenterupload.ts @@ -6,7 +6,7 @@ import fs = require('fs'); import { ToolRunner } from 'vsts-task-lib/toolrunner'; -var utils = require('./utils.js'); +import utils = require('./utils'); class UploadInfo { upload_id: string; @@ -164,37 +164,41 @@ function publishRelease(apiServer: string, releaseUrl: string, releaseNotes: str /** * If the input is a single file, upload this file without any processing. - * If the input is a dSYM folder, zip the parent and upload the zip so dSYM folder appears on the root of the archive - * If the input is a folder, zip the input folder so all files under this folder appears on the root of the archive + * If the input is a single folder, zip it's content. The archive name is the folder's name + * If the input is a set of folders, zip the folders so they appear on the root of the archive. The archive name is the parent folder's name. */ -function prepareSymbols(symbolsPath: string, packParentFolder: boolean): Q.Promise { - tl.debug("-- Prepare symbols") - let defer = Q.defer(); +function prepareSymbols(symbolsPaths: string[], symbolsRoot: string): Q.Promise { + tl.debug("-- Prepare symbols"); + let defer = Q.defer(); + + let zipFileName: string = null; + if (symbolsPaths.length === 1 && fs.statSync(symbolsPaths[0]).isFile()) { + // single file - Android source mapping txt file + tl.debug(`---- symbol file: ${symbolsPaths[0]}`) + defer.resolve(symbolsPaths[0]); + } else { + tl.debug(`---- Archiving ${symbolsPaths}`); + + // If symbol paths do not have anything common, e.g /a/b/c and /x/y/z, + // let's use some default name for the archive. + let zipName = symbolsRoot ? path.basename(symbolsRoot) : "symbols"; + tl.debug(`---- Zip file name=${zipName}`); - let stat = fs.statSync(symbolsPath); - if (stat.isFile() && !packParentFolder) { - // single file - Android source mapping txt file - tl.debug(`---- symbol file: ${symbolsPath}`) - defer.resolve(symbolsPath); - } else { - if (packParentFolder) { - tl.debug(`---- Take the parent folder of ${symbolsPath}`); - symbolsPath = path.dirname(symbolsPath); - } - - tl.debug(`---- Creating symbols from ${symbolsPath}`); - let zipStream = utils.createZipStream(symbolsPath, utils.isDsym(symbolsPath)); let workDir = tl.getVariable("System.DefaultWorkingDirectory"); - let zipName = path.join(workDir, `${path.basename(symbolsPath)}.zip`); - utils.createZipFile(zipStream, zipName). - then(() => { - tl.debug(`---- symbol file: ${zipName}`) - defer.resolve(zipName); - }); - } + let zipPath = path.join(workDir, `${zipName}.zip`); + tl.debug(`---- Zip file path=${zipPath}`); + + let zipStream = utils.createZipStream(symbolsPaths, symbolsRoot); + utils.createZipFile(zipStream, zipPath). + then(() => { + tl.debug(`---- symbol file: ${zipPath}`) + defer.resolve(zipPath); + }); + } - return defer.promise; -} + + return defer.promise; +} function beginSymbolUpload(apiServer: string, apiVersion: string, appSlug: string, symbol_type: string, token: string, userAgent: string): Q.Promise { tl.debug("-- Begin symbols upload") @@ -268,6 +272,42 @@ function commitSymbols(apiServer: string, apiVersion: string, appSlug: string, s return defer.promise; } +function expandSymbolsPaths(symbolsType: string, pattern: string, continueOnError: boolean, packParentFolder: boolean): string[] { + tl.debug("-- Expanding symbols path pattern to a list of paths"); + + let symbolsPaths: string[] = []; + + if (symbolsType === "Apple") { + // User can specifay a symbols path pattern that selects + // multiple dSYM folder paths for Apple application. + let dsymPaths = utils.resolvePaths(pattern, continueOnError, packParentFolder); + + dsymPaths.forEach(dsymFolder => { + if (dsymFolder) { + let folderPath = utils.checkAndFixFilePath(dsymFolder, "symbolsPath", continueOnError); + // The path can be null if continueIfSymbolsNotFound is true and the folder does not exist. + if (folderPath) { + symbolsPaths.push(folderPath); + } + } + }) + } else { + // For all other application types user can specifay a symbols path pattern + // that selects only one file or one folder. + let symbolsFile = utils.resolveSinglePath(pattern, continueOnError, packParentFolder); + + if (symbolsFile) { + let filePath = utils.checkAndFixFilePath(symbolsFile, "symbolsPath", continueOnError); + // The path can be null if continueIfSymbolsNotFound is true and the file/folder does not exist. + if (filePath) { + symbolsPaths.push(filePath); + } + } + } + + return symbolsPaths; +} + async function run() { try { tl.setResourcePath(path.join(__dirname, 'task.json')); @@ -331,7 +371,9 @@ async function run() { if (continueIfSymbolsNotFoundVariable && continueIfSymbolsNotFoundVariable.toLowerCase() === 'true') { continueIfSymbolsNotFound = true; } - let symbolsPath = utils.checkAndFixFilePath(utils.resolveSinglePath(symbolsPathPattern, continueIfSymbolsNotFound), "symbolsPath", continueIfSymbolsNotFound); + + // Expand symbols path pattern to a list of paths + let symbolsPaths = expandSymbolsPaths(symbolsType, symbolsPathPattern, continueIfSymbolsNotFound, packParentFolder); // Begin release upload let uploadInfo: UploadInfo = await beginReleaseUpload(effectiveApiServer, effectiveApiVersion, appSlug, apiToken, userAgent); @@ -346,9 +388,12 @@ async function run() { await publishRelease(effectiveApiServer, packageUrl, releaseNotes, distributionGroupId, apiToken, userAgent); // Uploading symbols - if (symbolsPath) { + if (symbolsPaths.length > 0) { + // Detect the common parent of all symbols paths to define the archive's internal folder structure. + let symbolsRoot = utils.findCommonParent(symbolsPaths); + // Prepare symbols - let symbolsFile = await prepareSymbols(symbolsPath, packParentFolder); + let symbolsFile = await prepareSymbols(symbolsPaths, symbolsRoot); // Begin preparing upload symbols let symbolsUploadInfo = await beginSymbolUpload(effectiveApiServer, effectiveApiVersion, appSlug, symbolsType, apiToken, userAgent);