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

Added before/after methods and test duration + deployment tests #1369

Merged
merged 9 commits into from
Jun 17, 2023
18 changes: 14 additions & 4 deletions src/api/Tools.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { API, GitExtension } from "./import/git";
import Crypto from 'crypto';
import { readFileSync } from "fs";
import path from "path";
import vscode from "vscode";
import path from "path"
import { API, GitExtension } from "./import/git";

export namespace Tools {
export class SqlError extends Error {
Expand Down Expand Up @@ -116,12 +118,12 @@ export namespace Tools {
return rows;
}

export function makeid() {
export function makeid(length: number = 8) {
let text = `O_`;
const possible =
`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789`;

for (let i = 0; i < 8; i++)
for (let i = 0; i < length; i++)
text += possible.charAt(Math.floor(Math.random() * possible.length));

return text;
Expand Down Expand Up @@ -195,4 +197,12 @@ export namespace Tools {
export function distinct(value: any, index: number, array: any[]) {
return array.indexOf(value) === index;
}

export function md5Hash(file: vscode.Uri): string {
const bytes = readFileSync(file.fsPath);
return Crypto.createHash("md5")
.update(bytes)
.digest("hex")
.toLowerCase();
}
}
12 changes: 1 addition & 11 deletions src/api/local/deployment.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import Crypto from 'crypto';
import { readFileSync } from 'fs';
import createIgnore, { Ignore } from 'ignore';
import path, { basename } from 'path';
import tar from 'tar';
Expand Down Expand Up @@ -358,7 +356,7 @@ export namespace Deployment {
const uploads: vscode.Uri[] = [];
for await (const file of localFiles) {
const remote = remoteMD5.find(e => e.path === file.path);
const md5 = md5Hash(file.uri);
const md5 = Tools.md5Hash(file.uri);
if (!remote || remote.md5 !== md5) {
uploads.push(file.uri);
}
Expand Down Expand Up @@ -483,14 +481,6 @@ export namespace Deployment {
};
}

function md5Hash(file: vscode.Uri): string {
const bytes = readFileSync(file.fsPath);
return Crypto.createHash("md5")
.update(bytes)
.digest("hex")
.toLowerCase();
}

function toRelative(root: vscode.Uri, file: vscode.Uri) {
return path.relative(root.path, file.path).replace(/\\/g, `/`);
}
Expand Down
4 changes: 2 additions & 2 deletions src/testing/connection.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import assert from "assert";
import { commands } from "vscode";
import { TestSuite } from ".";
import { instance } from "../instantiate";
import { commands } from "vscode";
import { CommandResult } from "../typings";

export const ConnectionSuite: TestSuite = {
export const ConnectionSuite: TestSuite = {
name: `Connection tests`,
tests: [
{name: `Test sendCommand`, test: async () => {
Expand Down
239 changes: 239 additions & 0 deletions src/testing/deployment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import assert from "assert";
import { randomInt } from "crypto";
import { existsSync } from "fs";
import ignore from "ignore";
import { EOL } from "os";
import { basename, posix } from "path";
import vscode from "vscode";
import { TestSuite } from ".";
import { Tools } from "../api/Tools";
import { Deployment } from "../api/local/deployment";
import { instance } from "../instantiate";
import { DeploymentMethod } from "../typings";

type FileInfo = {
md5: string
date: string
}

type FilesInfo = Map<string, FileInfo>;

class File {
readonly content: string[] = [];
localPath?: vscode.Uri;
remotePath?: string;

constructor(readonly name: string) {
this.changeContent();

}

changeContent() {
this.content.splice(0, this.content.length);
for (let line = 0; line < (5 + randomInt(41)); line++) {
this.content.push(Tools.makeid(10 + randomInt(100)));
}
}

getContent(){
return this.content.join(EOL);
}
}

type Folder = {
name: string
folders?: Folder[]
files?: File[]
localPath?: vscode.Uri
remotePath?: string;
}

const fakeProject: Folder = {
name: `DeleteMe_${Tools.makeid()}`,
folders: [
{ name: "folder1", files: [new File("file11.txt"), new File("file22.txt"), new File("file23.txt")] },
{
name: "folder2", files: [new File("file21.txt")], folders: [
{ name: "subfolder21", files: [new File("subfile211.txt"), new File("subfile212.txt")] },
{ name: "subfolder22", files: [new File("subfile221.txt"), new File("subfile222.txt"), new File("subfile223.txt")] }
]
},
{
name: "folder3", files: [new File("file31.txt"), new File("file32.txt"), new File("file33.txt"), new File("file34.txt"), new File("file35.txt")], folders: [
{
name: "subfolder32", files: [new File("subfile321.txt")], folders: [
{ name: "subsubfolder331", files: [new File("subsubfile3311.txt"), new File("subsubfile3312.txt")] },
{ name: "subsubfolder332", files: [new File("subsubfile3321.txt"), new File("subsubfile3322.txt"), new File("subsubfile3324'.txt"), new File("subsubfile3325.txt")] }
]
},
]
}
],
files: [
new File("rootFile1.txt"),
new File("rootFile2.txt"),
new File("rootFile3.txt")
],
}

export const DeploymentSuite: TestSuite = {
name: `Deployment tests`,
before: async () => {
const features = instance.getConnection()?.remoteFeatures;
assert.ok(features?.stat, "stat is required to run Deployment test suite");
assert.ok(features?.md5sum, "md5sum is required to run Deployment test suite");

const workspaceFolder = vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0] : undefined;
const tempDir = instance.getConfig()?.tempDir;
assert.ok(workspaceFolder, "No workspace folder to work with");
assert.ok(tempDir, "Cannot run deployment tests: no remote temp directory defined");

await createFolder(workspaceFolder.uri, tempDir, fakeProject);
assert.ok(fakeProject.localPath, "Project has no local path");
assert.ok(existsSync(fakeProject.localPath.fsPath), "Project local directory does not exist");
},
tests: [
{
name: `Test 'All' deployment`, test: async () => {
const locals = await getLocalFilesInfo();
const remotes = await deploy("all");
assertFilesInfoEquals(locals, remotes);
}
},
{
name: `Test 'Compare' deployment`, test: async () => {
createFile(fakeProject.localPath!, fakeProject.remotePath!, new File("new1.txt"));
createFile(fakeProject.folders![0].localPath!, fakeProject.folders![0].remotePath!, new File("newnew1.txt"));
createFile(fakeProject.folders![1].localPath!, fakeProject.folders![1].remotePath!, new File("newnew2.txt"));

await vscode.workspace.fs.delete(fakeProject.folders![2].files![0].localPath!, { useTrash: false });

await changeFile(fakeProject.files![0]);
await changeFile(fakeProject.folders![0].files![0]);

const oldRemotes = await getRemoteFilesInfo();
const locals = await getLocalFilesInfo();
const remotes = await deploy("compare");
assertFilesInfoEquals(locals, remotes);

let newFiles = 0;
let changed = 0;
let deleted = 0;
oldRemotes.forEach((oldInfo, file) => {
const newInfos = remotes.get(file);
if(newInfos && newInfos.date !== oldInfo.date){
changed++;
}
else if(!newInfos){
deleted++;
}
});

remotes.forEach((newInfo, file) => {
const oldInfo = oldRemotes.get(file);
if(!oldInfo){
newFiles++;
}
});

assert.strictEqual(newFiles, 3);
assert.strictEqual(changed, 2);
assert.strictEqual(deleted, 1);
}
}
],
after: async () => {
if (fakeProject.localPath && existsSync(fakeProject.localPath.fsPath)) {
await vscode.workspace.fs.delete(fakeProject.localPath, { recursive: true, useTrash: false });
}

if (fakeProject.remotePath && await instance.getContent()?.isDirectory(fakeProject.remotePath)) {
await instance.getConnection()?.sendCommand({ command: `rm -rf ${fakeProject.remotePath}` })
}
},
}

async function deploy(method: DeploymentMethod) {
assert.ok(fakeProject.localPath, "No local path");
assert.ok(fakeProject.remotePath, "No remote path");
const workspaceFolder = vscode.workspace.getWorkspaceFolder(fakeProject.localPath);
assert.ok(workspaceFolder, "No workspace folder");
//Deploy only the fake project
const ignoreRules = ignore().add([
`*`, //Ignore all
`!${basename(fakeProject.localPath.path)}/`, //Allow directory (required)
`!${basename(fakeProject.localPath.path)}/**` //Allow content
]);

assert.ok(await Deployment.deploy({ method, remotePath: fakeProject.remotePath, workspaceFolder, ignoreRules }), `"${method}" deployment failed`);
return await getRemoteFilesInfo();
}

async function createFolder(parent: vscode.Uri, remoteParent: string, folder: Folder) {
folder.localPath = vscode.Uri.joinPath(parent, folder.name);
folder.remotePath = posix.join(remoteParent, folder.name);
await vscode.workspace.fs.createDirectory(folder.localPath);

for (const file of folder.files || []) {
await createFile(folder.localPath!, folder.remotePath!, file);
}

for (const childFolder of folder.folders || []) {
await createFolder(folder.localPath!, folder.remotePath!, childFolder);
}
}

async function createFile(folder: vscode.Uri, remote: string, file: File): Promise<void> {
file.localPath = vscode.Uri.joinPath(folder, file.name);
file.remotePath = posix.join(remote, file.name);
await vscode.workspace.fs.writeFile(file.localPath, Buffer.from(file.content));
}

async function changeFile(file : File){
file.changeContent();
await vscode.workspace.fs.writeFile(file.localPath!, Buffer.from(file.content));
}

async function getLocalFilesInfo() {
const localFiles: FilesInfo = new Map;
for await (const file of await vscode.workspace.findFiles(new vscode.RelativePattern(fakeProject.localPath!, "**/*"))) {
const path = posix.join(basename(fakeProject.localPath!.path), posix.relative(fakeProject.localPath!.path, file.path));
localFiles.set(path, { date: "unused", md5: Tools.md5Hash(file) });
}
return localFiles;
}

async function getRemoteFilesInfo() {
const remoteFiles: FilesInfo = new Map;

//Get dates
const stat = (await instance.getConnection()?.sendCommand({
directory: fakeProject.remotePath,
command: `find . -type f -exec ${instance.getConnection()?.remoteFeatures.stat} '{}' --printf="%n %s\\n" \\;`
}));
assert.strictEqual(0, stat?.code, "Remote stat call failed");
stat?.stdout.split("\n")
.map(line => line.split(" "))
.forEach(([file, date]) => remoteFiles.set(file.substring(2), { date, md5: "" }));

//Get md5 sums
const md5sum = (await instance.getConnection()?.sendCommand({
directory: fakeProject.remotePath,
command: `${instance.getConnection()?.remoteFeatures.md5sum} $(find . -type f);`
}));
assert.strictEqual(0, md5sum?.code, "Remote md5sum call failed");
md5sum?.stdout.split("\n")
.map(line => line.split(/\s+/))
.forEach(([md5, file]) => remoteFiles.get(file.substring(2))!.md5 = md5);

return remoteFiles;
}

function assertFilesInfoEquals(locals: FilesInfo, remotes: FilesInfo) {
assert.strictEqual(locals.size, remotes.size, `Local (${locals.size}) and remote (${remotes.size}) files counts don't match`);
locals.forEach((info, file) => {
const remoteFile = remotes.get(file);
assert.ok(remoteFile, "Local file not found in remote files list");
assert.strictEqual(info.md5, remoteFile.md5, "Remote file hash doesn't match local's");
});
}
Loading