Add more tests for push/build/deploy commands (--dockerfile)

Change-type: patch
Signed-off-by: Paulo Castro <paulo@balena.io>
This commit is contained in:
Paulo Castro 2020-01-24 01:21:51 +00:00 committed by Paulo Castro
parent 58e7880f1d
commit 0f5f65e0d3
12 changed files with 248 additions and 132 deletions

View File

@ -20,7 +20,7 @@ import * as path from 'path';
import { NockMock, ScopeOpts } from './nock-mock';
const apiResponsePath = path.normalize(
export const apiResponsePath = path.normalize(
path.join(__dirname, 'test-data', 'api-response'),
);

View File

@ -17,10 +17,15 @@
import Bluebird = require('bluebird');
import * as _ from 'lodash';
import * as path from 'path';
import * as zlib from 'zlib';
import { NockMock } from './nock-mock';
export const builderResponsePath = path.normalize(
path.join(__dirname, 'test-data', 'builder-response'),
);
export class BuilderMock extends NockMock {
constructor() {
super('https://builder.balena-cloud.com');
@ -31,15 +36,17 @@ export class BuilderMock extends NockMock {
persist?: boolean;
responseBody: any;
responseCode: number;
checkURI: (uri: string) => Promise<void>;
checkBuildRequestBody: (requestBody: string | Buffer) => Promise<void>;
}) {
this.optPost(/^\/v3\/build($|[(?])/, opts).reply(async function(
_uri,
uri,
requestBody,
callback,
) {
let error: Error | null = null;
try {
await opts.checkURI(uri);
if (typeof requestBody === 'string') {
const gzipped = Buffer.from(requestBody, 'hex');
const gunzipped = await Bluebird.fromCallback<Buffer>(cb => {

View File

@ -15,16 +15,16 @@
* limitations under the License.
*/
import { configureBluebird } from '../../build/app-common';
configureBluebird();
// tslint:disable-next-line:no-var-requires
require('../config-tests'); // required for side effects
import { expect } from 'chai';
import { stripIndent } from 'common-tags';
import { fs } from 'mz';
import * as path from 'path';
import { URL } from 'url';
import { BalenaAPIMock } from '../balena-api-mock';
import { DockerMock } from '../docker-mock';
import { DockerMock, dockerResponsePath } from '../docker-mock';
import {
cleanOutput,
inspectTarStream,
@ -35,10 +35,27 @@ import {
const repoPath = path.normalize(path.join(__dirname, '..', '..'));
const projectsPath = path.join(repoPath, 'tests', 'test-data', 'projects');
const expectedResponses = {
'build-POST.json': [
'[Info] Building for amd64/nuc',
'[Info] Docker Desktop detected (daemon architecture: "x86_64")',
'[Info] Docker itself will determine and enable architecture emulation if required,',
'[Info] without balena-cli intervention and regardless of the --emulated option.',
'[Build] main Image size: 1.14 MB',
'[Success] Build succeeded!',
],
};
describe('balena build', function() {
let api: BalenaAPIMock;
let docker: DockerMock;
const commonQueryParams = [
['t', 'basic_main'],
['buildargs', '{}'],
['labels', ''],
];
this.beforeEach(() => {
api = new BalenaAPIMock();
docker = new DockerMock();
@ -61,31 +78,23 @@ describe('balena build', function() {
const expectedFiles: TarStreamFiles = {
'src/start.sh': { fileSize: 89, type: 'file' },
Dockerfile: { fileSize: 85, type: 'file' },
'Dockerfile-alt': { fileSize: 30, type: 'file' },
};
const responseBody = stripIndent`
{"stream":"Step 1/4 : FROM busybox"}
{"stream":"\\n"}
{"stream":" ---\\u003e 64f5d945efcc\\n"}
{"stream":"Step 2/4 : COPY ./src/start.sh /start.sh"}
{"stream":"\\n"}
{"stream":" ---\\u003e Using cache\\n"}
{"stream":" ---\\u003e 97098fc9d757\\n"}
{"stream":"Step 3/4 : RUN chmod a+x /start.sh"}
{"stream":"\\n"}
{"stream":" ---\\u003e Using cache\\n"}
{"stream":" ---\\u003e 33728e2e3f7e\\n"}
{"stream":"Step 4/4 : CMD [\\"/start.sh\\"]"}
{"stream":"\\n"}
{"stream":" ---\\u003e Using cache\\n"}
{"stream":" ---\\u003e 2590e3b11eaf\\n"}
{"aux":{"ID":"sha256:2590e3b11eaf739491235016b53fec5d209c81837160abdd267c8fe5005ff1bd"}}
{"stream":"Successfully built 2590e3b11eaf\\n"}
{"stream":"Successfully tagged basic_main:latest\\n"}`;
const responseFilename = 'build-POST.json';
const responseBody = await fs.readFile(
path.join(dockerResponsePath, responseFilename),
'utf8',
);
docker.expectPostBuild({
tag: 'basic_main',
responseCode: 200,
responseBody,
checkURI: async (uri: string) => {
const url = new URL(uri, 'http://test.net/');
const queryParams = Array.from(url.searchParams.entries());
expect(queryParams).to.have.deep.members(commonQueryParams);
},
checkBuildRequestBody: (buildRequestBody: string) =>
inspectTarStream(buildRequestBody, expectedFiles, projectPath, expect),
});
@ -99,12 +108,7 @@ describe('balena build', function() {
cleanOutput(out).map(line => line.replace(/\s{2,}/g, ' ')),
).to.include.members([
`[Info] Creating default composition with source: ${projectPath}`,
'[Info] Building for amd64/nuc',
'[Info] Docker Desktop detected (daemon architecture: "x86_64")',
'[Info] Docker itself will determine and enable architecture emulation if required,',
'[Info] without balena-cli intervention and regardless of the --emulated option.',
'[Build] main Image size: 1.14 MB',
'[Success] Build succeeded!',
...expectedResponses[responseFilename],
]);
});
});

View File

@ -15,16 +15,16 @@
* limitations under the License.
*/
import { configureBluebird } from '../../build/app-common';
configureBluebird();
// tslint:disable-next-line:no-var-requires
require('../config-tests'); // required for side effects
import { expect } from 'chai';
import { stripIndent } from 'common-tags';
import { fs } from 'mz';
import * as path from 'path';
import { URL } from 'url';
import { BalenaAPIMock } from '../balena-api-mock';
import { DockerMock } from '../docker-mock';
import { DockerMock, dockerResponsePath } from '../docker-mock';
import {
cleanOutput,
inspectTarStream,
@ -34,11 +34,31 @@ import {
const repoPath = path.normalize(path.join(__dirname, '..', '..'));
const projectsPath = path.join(repoPath, 'tests', 'test-data', 'projects');
const expectedResponses = {
'build-POST.json': [
'[Info] Building for armv7hf/raspberrypi3',
'[Info] Docker Desktop detected (daemon architecture: "x86_64")',
'[Info] Docker itself will determine and enable architecture emulation if required,',
'[Info] without balena-cli intervention and regardless of the --emulated option.',
'[Build] main Image size: 1.14 MB',
'[Info] Creating release...',
'[Info] Pushing images to registry...',
'[Info] Saving release...',
'[Success] Deploy succeeded!',
'[Success] Release: 09f7c3e1fdec609be818002299edfc2a',
],
};
describe('balena deploy', function() {
let api: BalenaAPIMock;
let docker: DockerMock;
const commonQueryParams = [
['t', 'basic_main'],
['buildargs', '{}'],
['labels', ''],
];
this.beforeEach(() => {
api = new BalenaAPIMock();
docker = new DockerMock();
@ -78,31 +98,23 @@ describe('balena deploy', function() {
const expectedFiles: TarStreamFiles = {
'src/start.sh': { fileSize: 89, type: 'file' },
Dockerfile: { fileSize: 85, type: 'file' },
'Dockerfile-alt': { fileSize: 30, type: 'file' },
};
const responseBody = stripIndent`
{"stream":"Step 1/4 : FROM busybox"}
{"stream":"\\n"}
{"stream":" ---\\u003e 64f5d945efcc\\n"}
{"stream":"Step 2/4 : COPY ./src/start.sh /start.sh"}
{"stream":"\\n"}
{"stream":" ---\\u003e Using cache\\n"}
{"stream":" ---\\u003e 97098fc9d757\\n"}
{"stream":"Step 3/4 : RUN chmod a+x /start.sh"}
{"stream":"\\n"}
{"stream":" ---\\u003e Using cache\\n"}
{"stream":" ---\\u003e 33728e2e3f7e\\n"}
{"stream":"Step 4/4 : CMD [\\"/start.sh\\"]"}
{"stream":"\\n"}
{"stream":" ---\\u003e Using cache\\n"}
{"stream":" ---\\u003e 2590e3b11eaf\\n"}
{"aux":{"ID":"sha256:2590e3b11eaf739491235016b53fec5d209c81837160abdd267c8fe5005ff1bd"}}
{"stream":"Successfully built 2590e3b11eaf\\n"}
{"stream":"Successfully tagged basic_main:latest\\n"}`;
const responseFilename = 'build-POST.json';
const responseBody = await fs.readFile(
path.join(dockerResponsePath, responseFilename),
'utf8',
);
docker.expectPostBuild({
tag: 'basic_main',
responseCode: 200,
responseBody,
checkURI: async (uri: string) => {
const url = new URL(uri, 'http://test.net/');
const queryParams = Array.from(url.searchParams.entries());
expect(queryParams).to.have.deep.members(commonQueryParams);
},
checkBuildRequestBody: (buildRequestBody: string) =>
inspectTarStream(buildRequestBody, expectedFiles, projectPath, expect),
});
@ -116,16 +128,7 @@ describe('balena deploy', function() {
cleanOutput(out).map(line => line.replace(/\s{2,}/g, ' ')),
).to.include.members([
`[Info] Creating default composition with source: ${projectPath}`,
'[Info] Building for armv7hf/raspberrypi3',
'[Info] Docker Desktop detected (daemon architecture: "x86_64")',
'[Info] Docker itself will determine and enable architecture emulation if required,',
'[Info] without balena-cli intervention and regardless of the --emulated option.',
'[Build] main Image size: 1.14 MB',
'[Info] Creating release...',
'[Info] Pushing images to registry...',
'[Info] Saving release...',
'[Success] Deploy succeeded!',
'[Success] Release: 09f7c3e1fdec609be818002299edfc2a',
...expectedResponses[responseFilename],
]);
});
});

View File

@ -18,7 +18,7 @@
import { expect } from 'chai';
import * as path from 'path';
import { BalenaAPIMock } from '../../balena-api-mock';
import { apiResponsePath, BalenaAPIMock } from '../../balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers';
const HELP_RESPONSE = `
@ -31,10 +31,6 @@ Examples:
\t$ balena device 7cf02a6
`;
const apiResponsePath = path.normalize(
path.join(__dirname, '..', '..', 'test-data', 'api-response'),
);
describe('balena device', function() {
let api: BalenaAPIMock;

View File

@ -18,7 +18,7 @@
import { expect } from 'chai';
import * as path from 'path';
import { BalenaAPIMock } from '../../balena-api-mock';
import { apiResponsePath, BalenaAPIMock } from '../../balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers';
const HELP_RESPONSE = `
@ -40,10 +40,6 @@ Options:
--application, -a, --app <application> application name
`;
const apiResponsePath = path.normalize(
path.join(__dirname, '..', '..', 'test-data', 'api-response'),
);
describe('balena devices', function() {
let api: BalenaAPIMock;

View File

@ -15,17 +15,16 @@
* limitations under the License.
*/
import { configureBluebird } from '../../build/app-common';
configureBluebird();
// tslint:disable-next-line:no-var-requires
require('../config-tests'); // required for side effects
import { expect } from 'chai';
import { fs } from 'mz';
import * as path from 'path';
import { URL } from 'url';
import { BalenaAPIMock } from '../balena-api-mock';
import { BuilderMock } from '../builder-mock';
// import { DockerMock } from '../docker-mock';
import { BuilderMock, builderResponsePath } from '../builder-mock';
import {
cleanOutput,
inspectTarStream,
@ -35,14 +34,60 @@ import {
const repoPath = path.normalize(path.join(__dirname, '..', '..'));
const projectsPath = path.join(repoPath, 'tests', 'test-data', 'projects');
const builderResponsePath = path.normalize(
path.join(__dirname, '..', 'test-data', 'builder-response'),
);
const expectedResponses = {
'build-POST-v3.json': [
'[Info] Starting build for testApp, user gh_user',
'[Info] Dashboard link: https://dashboard.balena-cloud.com/apps/1301645/devices',
'[Info] Building on arm01',
'[Info] Pulling previous images for caching purposes...',
'[Success] Successfully pulled cache images',
'[main] Step 1/4 : FROM busybox',
'[main] ---> 76aea0766768',
'[main] Step 2/4 : COPY ./src/start.sh /start.sh',
'[main] ---> b563ad6a0801',
'[main] Step 3/4 : RUN chmod a+x /start.sh',
'[main] ---> Running in 10d4ddc40bfc',
'[main] Removing intermediate container 10d4ddc40bfc',
'[main] ---> 82e98871a32c',
'[main] Step 4/4 : CMD ["/start.sh"]',
'[main] ---> Running in 0682894e13eb',
'[main] Removing intermediate container 0682894e13eb',
'[main] ---> 889ccb6afc7c',
'[main] Successfully built 889ccb6afc7c',
'[Info] Uploading images',
'[Success] Successfully uploaded images',
'[Info] Built on arm01',
'[Success] Release successfully created!',
'[Info] Release: 05a24b5b034c9f95f25d4d74f0593bea (id: 1220245)',
'[Info] ┌─────────┬────────────┬────────────┐',
'[Info] │ Service │ Image Size │ Build Time │',
'[Info] ├─────────┼────────────┼────────────┤',
'[Info] │ main │ 1.32 MB │ 11 seconds │',
'[Info] └─────────┴────────────┴────────────┘',
'[Info] Build finished in 20 seconds',
],
};
function tweakOutput(out: string[]): string[] {
return cleanOutput(out).map(line =>
line.replace(/\s{2,}/g, ' ').replace(/in \d+? seconds/, 'in 20 seconds'),
);
}
describe('balena push', function() {
let api: BalenaAPIMock;
let builder: BuilderMock;
const commonQueryParams = [
['owner', 'bob'],
['app', 'testApp'],
['dockerfilePath', ''],
['emulated', 'false'],
['nocache', 'false'],
['headless', 'false'],
];
this.beforeEach(() => {
api = new BalenaAPIMock();
builder = new BuilderMock();
@ -57,20 +102,27 @@ describe('balena push', function() {
builder.done();
});
it('should create the expected tar stream', async () => {
it('should create the expected tar stream (single container)', async () => {
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
const expectedFiles: TarStreamFiles = {
'src/start.sh': { fileSize: 89, type: 'file' },
Dockerfile: { fileSize: 85, type: 'file' },
'Dockerfile-alt': { fileSize: 30, type: 'file' },
};
const responseFilename = 'build-POST-v3.json';
const responseBody = await fs.readFile(
path.join(builderResponsePath, 'build-POST-v3.json'),
path.join(builderResponsePath, responseFilename),
'utf8',
);
builder.expectPostBuild({
responseCode: 200,
responseBody,
checkURI: async (uri: string) => {
const url = new URL(uri, 'http://test.net/');
const queryParams = Array.from(url.searchParams.entries());
expect(queryParams).to.have.deep.members(commonQueryParams);
},
checkBuildRequestBody: (buildRequestBody: string | Buffer) =>
inspectTarStream(buildRequestBody, expectedFiles, projectPath, expect),
});
@ -80,42 +132,49 @@ describe('balena push', function() {
);
expect(err).to.have.members([]);
expect(
cleanOutput(out).map(line =>
line
.replace(/\s{2,}/g, ' ')
.replace(/in \d+? seconds/, 'in 20 seconds'),
),
).to.include.members([
'[Info] Starting build for testApp, user gh_user',
'[Info] Dashboard link: https://dashboard.balena-cloud.com/apps/1301645/devices',
'[Info] Building on arm01',
'[Info] Pulling previous images for caching purposes...',
'[Success] Successfully pulled cache images',
'[main] Step 1/4 : FROM busybox',
'[main] ---> 76aea0766768',
'[main] Step 2/4 : COPY ./src/start.sh /start.sh',
'[main] ---> b563ad6a0801',
'[main] Step 3/4 : RUN chmod a+x /start.sh',
'[main] ---> Running in 10d4ddc40bfc',
'[main] Removing intermediate container 10d4ddc40bfc',
'[main] ---> 82e98871a32c',
'[main] Step 4/4 : CMD ["/start.sh"]',
'[main] ---> Running in 0682894e13eb',
'[main] Removing intermediate container 0682894e13eb',
'[main] ---> 889ccb6afc7c',
'[main] Successfully built 889ccb6afc7c',
'[Info] Uploading images',
'[Success] Successfully uploaded images',
'[Info] Built on arm01',
'[Success] Release successfully created!',
'[Info] Release: 05a24b5b034c9f95f25d4d74f0593bea (id: 1220245)',
'[Info] ┌─────────┬────────────┬────────────┐',
'[Info] │ Service │ Image Size │ Build Time │',
'[Info] ├─────────┼────────────┼────────────┤',
'[Info] │ main │ 1.32 MB │ 11 seconds │',
'[Info] └─────────┴────────────┴────────────┘',
'[Info] Build finished in 20 seconds',
]);
expect(tweakOutput(out)).to.include.members(
expectedResponses[responseFilename],
);
});
it('should create the expected tar stream (alternative Dockerfile)', async () => {
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
const expectedFiles: TarStreamFiles = {
'src/start.sh': { fileSize: 89, type: 'file' },
Dockerfile: { fileSize: 85, type: 'file' },
'Dockerfile-alt': { fileSize: 30, type: 'file' },
};
const responseFilename = 'build-POST-v3.json';
const responseBody = await fs.readFile(
path.join(builderResponsePath, responseFilename),
'utf8',
);
builder.expectPostBuild({
responseCode: 200,
responseBody,
checkURI: async (uri: string) => {
const url = new URL(uri, 'http://test.net/');
const queryParams = Array.from(url.searchParams.entries());
expect(queryParams).to.have.deep.members(
commonQueryParams.map(i =>
i[0] === 'dockerfilePath'
? ['dockerfilePath', 'Dockerfile-alt']
: i,
),
);
},
checkBuildRequestBody: (buildRequestBody: string | Buffer) =>
inspectTarStream(buildRequestBody, expectedFiles, projectPath, expect),
});
const { out, err } = await runCommand(
`push testApp --source ${projectPath} --dockerfile Dockerfile-alt`,
);
expect(err).to.have.members([]);
expect(tweakOutput(out)).to.include.members(
expectedResponses[responseFilename],
);
});
});

33
tests/config-tests.ts Normal file
View File

@ -0,0 +1,33 @@
/**
* @license
* Copyright 2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { configureBluebird, setMaxListeners } from '../build/app-common';
configureBluebird();
setMaxListeners(35); // it appears that 'nock' adds a bunch of listeners - bug?
// SL: Looks like it's not nock causing this, as have seen the problem triggered from help.spec,
// which is not using nock. Perhaps mocha/chai? (unlikely), or something in the CLI?
import { config as chaiCfg } from 'chai';
function configChai() {
chaiCfg.showDiff = true;
// enable diff comparison of large objects / arrays
chaiCfg.truncateThreshold = 0;
}
configChai();

View File

@ -20,7 +20,7 @@ import * as path from 'path';
import { NockMock, ScopeOpts } from './nock-mock';
const dockerResponsePath = path.normalize(
export const dockerResponsePath = path.normalize(
path.join(__dirname, 'test-data', 'docker-response'),
);
@ -70,14 +70,16 @@ export class DockerMock extends NockMock {
responseBody: any;
responseCode: number;
tag: string;
checkURI: (uri: string) => Promise<void>;
checkBuildRequestBody: (requestBody: string) => Promise<void>;
}) {
this.optPost(
new RegExp(`^/build\\?t=${_.escapeRegExp(opts.tag)}&`),
opts,
).reply(async function(_uri, requestBody, cb) {
).reply(async function(uri, requestBody, cb) {
let error: Error | null = null;
try {
await opts.checkURI(uri);
if (typeof requestBody === 'string') {
await opts.checkBuildRequestBody(requestBody);
} else {

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2019 Balena Ltd.
* Copyright 2019-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,6 +15,9 @@
* limitations under the License.
*/
// tslint:disable-next-line:no-var-requires
require('./config-tests'); // required for side effects
import intercept = require('intercept-stdout');
import * as _ from 'lodash';
import { fs } from 'mz';
@ -26,12 +29,6 @@ import * as tar from 'tar-stream';
import { streamToBuffer } from 'tar-utils';
import * as balenaCLI from '../build/app';
import { configureBluebird, setMaxListeners } from '../build/app-common';
configureBluebird();
setMaxListeners(35); // it appears that 'nock' adds a bunch of listeners - bug?
// SL: Looks like it's not nock causing this, as have seen the problem triggered from help.spec,
// which is not using nock. Perhaps mocha/chai? (unlikely), or something in the CLI?
export const runCommand = async (cmd: string) => {
const preArgs = [process.argv[0], path.join(process.cwd(), 'bin', 'balena')];

View File

@ -0,0 +1,18 @@
{"stream":"Step 1/4 : FROM busybox"}
{"stream":"\n"}
{"stream":" ---\u003e 64f5d945efcc\n"}
{"stream":"Step 2/4 : COPY ./src/start.sh /start.sh"}
{"stream":"\n"}
{"stream":" ---\u003e Using cache\n"}
{"stream":" ---\u003e 97098fc9d757\n"}
{"stream":"Step 3/4 : RUN chmod a+x /start.sh"}
{"stream":"\n"}
{"stream":" ---\u003e Using cache\n"}
{"stream":" ---\u003e 33728e2e3f7e\n"}
{"stream":"Step 4/4 : CMD [\"/start.sh\"]"}
{"stream":"\n"}
{"stream":" ---\u003e Using cache\n"}
{"stream":" ---\u003e 2590e3b11eaf\n"}
{"aux":{"ID":"sha256:2590e3b11eaf739491235016b53fec5d209c81837160abdd267c8fe5005ff1bd"}}
{"stream":"Successfully built 2590e3b11eaf\n"}
{"stream":"Successfully tagged basic_main:latest\n"}

View File

@ -0,0 +1 @@
alternative Dockerfile (basic)