gulp-msbuild
Version:
msbuild plugin for gulp. Inspired by grunt-msbuild.
313 lines (245 loc) • 11.8 kB
text/typescript
import { MSBuildRunner } from '../src/msbuild/msbuild-runner';
import { MSBuildOptions } from '../src/msbuild/msbuild-options';
import { spawn } from 'child_process';
import { EventEmitter } from 'events';
import * as fs from 'fs';
import { glob } from 'glob';
// Mock modules
jest.mock('child_process');
jest.mock('fs');
jest.mock('glob');
describe('MSBuildRunner', () => {
let mockOptions: MSBuildOptions;
let mockFile: any;
let mockStream: any;
let mockCallback: jest.Mock;
let mockChildProcess: EventEmitter;
const mockSpawn = spawn as jest.MockedFunction<typeof spawn>;
const mockFs = fs as jest.Mocked<typeof fs>;
const mockGlob = glob as jest.MockedFunction<typeof glob>;
beforeEach(() => {
jest.clearAllMocks();
// Suppress console output in tests
jest.spyOn(console, 'log').mockImplementation();
jest.spyOn(console, 'error').mockImplementation();
mockOptions = new MSBuildOptions({
msbuildPath: 'C:\\msbuild.exe' as any,
verbosity: 'normal',
stdout: false,
stderr: false,
errorOnFail: false,
logCommand: false
});
mockFile = { path: 'C:\\test.sln' };
mockStream = { push: jest.fn() };
mockCallback = jest.fn();
// Create a mock child process
mockChildProcess = new EventEmitter();
mockSpawn.mockReturnValue(mockChildProcess as any);
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('startMsBuildTask', () => {
it('should be defined', () => {
expect(MSBuildRunner.startMsBuildTask).toBeDefined();
});
it('should spawn msbuild process', () => {
MSBuildRunner.startMsBuildTask(mockOptions, mockFile, mockStream, mockCallback);
expect(mockSpawn).toHaveBeenCalled();
const spawnArgs = mockSpawn.mock.calls[0];
expect(spawnArgs[0]).toContain('msbuild.exe');
expect(spawnArgs[1]).toContain('C:\\test.sln');
});
it('should handle successful build (exit code 0)', (done) => {
MSBuildRunner.startMsBuildTask(mockOptions, mockFile, mockStream, (err: Error | null) => {
expect(err).toBeUndefined();
done();
});
// Simulate successful exit
setImmediate(() => mockChildProcess.emit('exit', 0, null));
});
it('should handle build failure with errorOnFail option', (done) => {
mockOptions.errorOnFail = true;
MSBuildRunner.startMsBuildTask(mockOptions, mockFile, mockStream, (err: Error | null) => {
expect(err).toBeDefined();
expect(err?.message).toContain('MSBuild failed with code 1');
done();
});
setImmediate(() => mockChildProcess.emit('exit', 1, null));
});
it('should handle process killed by signal with errorOnFail', (done) => {
mockOptions.errorOnFail = true;
MSBuildRunner.startMsBuildTask(mockOptions, mockFile, mockStream, (err: Error | null) => {
expect(err).toBeDefined();
expect(err?.message).toContain('killed with signal');
done();
});
setImmediate(() => mockChildProcess.emit('exit', null, 'SIGTERM'));
});
it('should handle process error', (done) => {
const testError = new Error('Process error');
MSBuildRunner.startMsBuildTask(mockOptions, mockFile, mockStream, (err: Error | null) => {
expect(err).toBeUndefined();
done();
});
setImmediate(() => mockChildProcess.emit('error', testError));
});
it('should handle process error with errorOnFail', (done) => {
mockOptions.errorOnFail = true;
const testError = new Error('Process error');
MSBuildRunner.startMsBuildTask(mockOptions, mockFile, mockStream, (err: Error | null) => {
expect(err).toBe(testError);
done();
});
setImmediate(() => mockChildProcess.emit('error', testError));
});
it('should not callback on exit after error already closed process', (done) => {
const testError = new Error('Process error');
MSBuildRunner.startMsBuildTask(mockOptions, mockFile, mockStream, (err: Error | null) => {
// Should only be called once from error, not from exit
expect(err).toBeUndefined();
// Try to emit exit after error - should be ignored
mockChildProcess.emit('exit', 1, null);
setTimeout(done, 50);
});
setImmediate(() => {
mockChildProcess.emit('error', testError);
});
});
it('should handle error event without error object', (done) => {
MSBuildRunner.startMsBuildTask(mockOptions, mockFile, mockStream, (err: Error | null) => {
expect(err).toBeUndefined();
done();
});
setImmediate(() => {
// Emit error event with null/undefined error
mockChildProcess.emit('error', null);
});
});
it('should handle exit event after error was already handled', (done) => {
const testError = new Error('Test error');
let callbackCount = 0;
MSBuildRunner.startMsBuildTask(mockOptions, mockFile, mockStream, () => {
callbackCount++;
if (callbackCount === 1) {
// First call from error
setTimeout(() => {
// After some time, verify callback wasn't called again
expect(callbackCount).toBe(1);
done();
}, 50);
}
});
setImmediate(() => {
mockChildProcess.emit('error', testError);
// Try to emit exit - should be ignored due to closed flag
setImmediate(() => {
mockChildProcess.emit('exit', 0, null);
});
});
});
it('should handle second error event after first error (closed guard)', (done) => {
const testError1 = new Error('First error');
const testError2 = new Error('Second error');
let callbackCount = 0;
MSBuildRunner.startMsBuildTask(mockOptions, mockFile, mockStream, () => {
callbackCount++;
if (callbackCount === 1) {
// First call from first error
setTimeout(() => {
// Verify callback wasn't called again from second error
expect(callbackCount).toBe(1);
done();
}, 50);
}
});
setImmediate(() => {
mockChildProcess.emit('error', testError1);
// Try to emit another error - should hit the closed guard and return early
setImmediate(() => {
mockChildProcess.emit('error', testError2);
});
});
});
it('should not call callback twice on error then exit', (done) => {
const testError = new Error('Process error');
let callCount = 0;
MSBuildRunner.startMsBuildTask(mockOptions, mockFile, mockStream, () => {
callCount++;
expect(callCount).toBe(1);
setTimeout(done, 10);
});
setImmediate(() => {
mockChildProcess.emit('error', testError);
mockChildProcess.emit('exit', 1, null);
});
});
it('should log command when logCommand is true', () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
mockOptions.logCommand = true;
MSBuildRunner.startMsBuildTask(mockOptions, mockFile, mockStream, mockCallback);
expect(consoleSpy).toHaveBeenCalledWith(
expect.anything(),
expect.stringContaining('msbuild.exe'),
expect.any(String)
);
consoleSpy.mockRestore();
});
it('should emit published files when emitPublishedFiles is true', (done) => {
mockOptions.emitPublishedFiles = true;
mockOptions.publishDirectory = 'C:\\output';
const mockFiles = [
'C:\\output\\file1.txt',
'C:\\output\\file2.txt'
];
mockGlob.mockResolvedValue(mockFiles as any);
mockFs.statSync.mockReturnValue({ isFile: () => true } as any);
mockFs.readFileSync.mockReturnValue(Buffer.from('test content'));
MSBuildRunner.startMsBuildTask(mockOptions, mockFile, mockStream, (err: Error | null) => {
expect(err).toBeUndefined();
expect(mockStream.push).toHaveBeenCalledTimes(2);
done();
});
setImmediate(() => mockChildProcess.emit('exit', 0, null));
});
it('should handle glob errors when emitting published files', (done) => {
mockOptions.emitPublishedFiles = true;
mockOptions.publishDirectory = 'C:\\output';
mockOptions.errorOnFail = false;
mockGlob.mockRejectedValue(new Error('Glob error'));
MSBuildRunner.startMsBuildTask(mockOptions, mockFile, mockStream, (err: Error | null) => {
expect(err).toBeDefined();
done();
});
setImmediate(() => mockChildProcess.emit('exit', 0, null));
});
it('should skip non-file entries when emitting published files', (done) => {
mockOptions.emitPublishedFiles = true;
mockOptions.publishDirectory = 'C:\\output';
const mockFiles = ['C:\\output\\file1.txt'];
mockGlob.mockResolvedValue(mockFiles as any);
mockFs.statSync.mockReturnValue({ isFile: () => false } as any);
MSBuildRunner.startMsBuildTask(mockOptions, mockFile, mockStream, (err: Error | null) => {
expect(err).toBeUndefined();
expect(mockStream.push).not.toHaveBeenCalled();
done();
});
setImmediate(() => mockChildProcess.emit('exit', 0, null));
});
it('should use stdout and stderr options', () => {
mockOptions.stdout = true;
mockOptions.stderr = true;
MSBuildRunner.startMsBuildTask(mockOptions, mockFile, mockStream, mockCallback);
const spawnOptions = mockSpawn.mock.calls[0][2];
expect(spawnOptions?.stdio).toEqual(['ignore', 'inherit', 'inherit']);
});
it('should ignore stdout and stderr when options are false', () => {
mockOptions.stdout = false;
mockOptions.stderr = false;
MSBuildRunner.startMsBuildTask(mockOptions, mockFile, mockStream, mockCallback);
const spawnOptions = mockSpawn.mock.calls[0][2];
expect(spawnOptions?.stdio).toEqual(['ignore', 'ignore', 'ignore']);
});
});
});