Commit 7d7cca1a authored by Renato Figueiro Maia's avatar Renato Figueiro Maia
Browse files

Mockup de driver do SGA para testes.

[SOMA-6916][SOMA-6857]
parent 3c97ec20
local dkjson = require("dkjson")
local socket = require("socket.core")
local testdir = os.getenv("SGA_TESTDIR")
local failures = assert(io.open(testdir.."/driver_failures.txt", "w"))
local function expect(condition, message, method)
if not condition then
failures:write(string.format("driver.%s: %s\n", method, message))
failures:flush()
end
end
local returns = assert(loadfile(testdir.."/driver_mockup.lua"))
returns = returns(socket.gettime, expect)
local privatekey = {}
local knownjobs = {}
local function checkargs(name, types, ...)
local count = select("#", ...)
expect(count == #types,
"got "..count.." arguments, instead of "..#types,
name)
for i = 1, count do
local value = select(i, ...)
local expected = types[i]
if expected == "job" then
expect(type(value) == "table",
"'job' is not a table",
name)
expect(type(value.data) == "table",
"'job.data' is not a table",
name)
if knownjobs[value] then
for field, expected in pairs(knownjobs[value]) do
expect(value[field] == expected,
"'job."..field.."' is wrong",
name)
end
expect(value.data[privatekey],
"a table field in 'job.data' was lost",
name)
expect(value.data.mock_driver_field == "mock driver field",
"'job.data.mock_driver_field' is wrong",
name)
else
knownjobs[value] = {
jid = value.jid,
cmd_id = value.cmd_id,
cmd_string = value.cmd_string,
parameters = value.parameters,
sandboxes = value.sandboxes,
data = value.data,
}
value.data.mock_driver_field = "mock driver field"
value.data[privatekey] = "private value"
end
elseif expected then
expect(type(value) == expected,
"argument #"..i.." is "..type(value)..", instead of "..expected,
name)
end
end
end
local expectedargs = {
get_nodes = {},
get_nodes_status = {},
list_path = {"string"},
check_path = {"string"},
execute_command = {"job", "string", "string"},
is_command_done = {"job"},
cleanup_job = {"job"},
actions = {
status = {"job", "string", "table"},
terminate = {"job", "string", "table"},
},
}
local function newemptylist()
return {}
end
returns.get_nodes = returns.get_nodes or newemptylist
returns.get_nodes_status = returns.get_nodes_status or newemptylist
local instance = { [privatekey] = {} }
local function createMockup(namespace, expected, returns, prefix)
for name, types in pairs(expected) do
if #types == 0 and next(types) then
namespace[name] = createMockup({}, types, returns[name] or {}, prefix..name..".")
else
local fullname = prefix..name
local getter = returns[name] or function ()
expect(false, "unexpected call", fullname)
return nil, "TEST_FAIL(unexpected call)"
end
namespace[name] = function (self, ...)
expect(self[privatekey] == instance[privatekey], "invalid 'self'", fullname)
checkargs(fullname, types, ...)
return getter(self, ...)
end
end
end
return namespace
end
local driver = createMockup({}, expectedargs, returns, "")
function driver.new()
return instance
end
return driver
......@@ -19,6 +19,7 @@
"author": "Renato Maia",
"license": "ISC",
"devDependencies": {
"chakram": "^1.5.0",
"eslint": "^7.15.0",
"eslint-config-prettier": "^7.0.0",
"eslint-config-standard": "^16.0.2",
......@@ -27,9 +28,8 @@
"eslint-plugin-prettier": "^3.3.0",
"eslint-plugin-promise": "^4.2.1",
"express": "^4.17.1",
"frisby": "^2.1.3",
"jasmine": "^3.6.3",
"jasmine-expect": "^5.0.0",
"prettier": "2.2.1"
"prettier": "^2.2.1"
}
}
"use strict";
const chakram = require("chakram");
const events = require("events");
const frisby = require("frisby");
const utils = require("./utils");
describe("Started SGA", function () {
......@@ -9,39 +9,137 @@ describe("Started SGA", function () {
it("should terminate on shutdown after registration", async () => {
const config = utils.fillSgaConfig();
const mock = new utils.RegistryService(config);
const regMockup = new utils.RegistryService(config);
const sga = new utils.SgaDaemon(config);
await mock.start();
await regMockup.start();
await sga.start();
const [req] = await events.once(mock.events, "register");
const [regReq] = await events.once(regMockup.events, "register");
utils.checkRegistration(regReq, config);
frisby.get(req.body.actions.shutdown).expect("status", 200);
const statRes = await chakram.get(regReq.body.actions.shutdown);
chakram.expect(statRes).to.have.status(200);
const [exitVal] = await sga.terminated();
expect(exitVal).toEqual(0);
});
it("should retry to register when server is down", async () => {
const mockPort = 55555;
const mockBaseUri = `http://localhost:${mockPort}`;
const regMockupPort = 55555;
const regMockupBaseUri = `http://localhost:${regMockupPort}`;
const config = utils.fillSgaConfig({
csbase_server: mockBaseUri,
csbase_server: regMockupBaseUri,
register_retry_s: 0.1,
});
const mock = new utils.RegistryService(config, undefined, mockPort);
const regMockup = new utils.RegistryService(
config,
undefined,
regMockupPort
);
const sga = new utils.SgaDaemon(config);
await sga.start();
await utils.waitMsec(200);
await mock.start();
await regMockup.start();
const [req] = await events.once(mock.events, "register");
const [regReq] = await events.once(regMockup.events, "register");
utils.checkRegistration(regReq, config);
frisby.get(req.body.actions.shutdown).expect("status", 200);
const statRes = await chakram.get(regReq.body.actions.shutdown);
chakram.expect(statRes).to.have.status(200);
const [exitVal] = await sga.terminated();
expect(exitVal).toEqual(0);
});
it("should receive requests to execute and inform its completion", async () => {
const jobBody = utils.fillJobBody();
const driverMockup = {
execute_command: `
expect(job.cmd_id == ${JSON.stringify(jobBody.cmd_id)})
expect(cmd_string == ${JSON.stringify(jobBody.cmd_string)})
expect(user_token == ${JSON.stringify(
jobBody.parameters.csbase_command_user_token
)})
expect(not self.jobDataRef)
self.jobDataRef = job.data
job.data.start = now()
return true
`,
is_command_done: `
expect(self.jobDataRef == job.data)
if not job.data.start or now()-job.data.start < 0.1 then
return false
end
job.data.start = false
return true, 0.07, 0.03, 0.01
`,
cleanup_job: `
expect(self.jobDataRef == job.data)
expect(not self.jobCleanup)
self.jobCleanup = true
`,
"actions.status": `
expect(self.jobDataRef == job.data)
expect(action == "status")
return true, {
someStatus = "some status",
otherStatus = "other status",
anotherStatus = "another status",
nestedStatus = {
someNestedStatus = "some nested status",
otherNestedStatus = "other nested status",
anotherNestedStatus = "another nested status",
}
}
`,
};
async function checkStatus(url) {
const statRes = await chakram.get(url);
chakram.expect(statRes).to.have.status(200);
chakram.expect(statRes).to.comprise.of.json({
someStatus: "some status",
otherStatus: "other status",
anotherStatus: "another status",
nestedStatus: {
someNestedStatus: "some nested status",
otherNestedStatus: "other nested status",
anotherNestedStatus: "another nested status",
},
});
}
const config = utils.fillSgaConfig();
const regMockup = new utils.RegistryService(config);
const sga = new utils.SgaDaemon(config, driverMockup);
spyOn(regMockup, "heartbeat").and.callThrough();
spyOn(regMockup, "status").and.callThrough();
await regMockup.start();
await sga.start();
const [regReq] = await events.once(regMockup.events, "register");
utils.checkRegistration(regReq, config);
const execRes = await chakram.post(regReq.body.actions.job, jobBody);
chakram.expect(execRes).to.have.status(201);
await checkStatus(execRes.body.actions.status);
const [compReq] = await events.once(regMockup.events, "completion");
expect(compReq.body).toEqual({
cmd_id: jobBody.cmd_id,
walltime_s: 0.07,
usertime_s: 0.03,
systime_s: 0.01,
});
expect(regMockup.heartbeat).toHaveBeenCalled();
expect(regMockup.status).toHaveBeenCalled();
await checkStatus(execRes.body.actions.status);
});
});
......@@ -21,7 +21,7 @@ const sgaConfig = {
algorithm_root_dir: path.join(testDir, "algorithms"),
runtime_data_dir: path.join(testDir, "runtime"),
sandbox_root_dir: path.join(testDir, "sandbox"),
driver: "sga.driver.posix",
driver: "sga.driver.mockup",
resources: ["some resource", "another resource", "yet another resource"],
extra_config: {
some_config: "some configuration",
......@@ -40,6 +40,7 @@ function fillSgaConfig(values) {
function writeAsLua(stream, value, prefix = "") {
switch (typeof value) {
case "boolean":
case "number":
{
stream.write(value.toString());
......@@ -80,15 +81,59 @@ function writeAsLua(stream, value, prefix = "") {
let sgaDaemons = new Set();
let registryMocks = new Set();
const driverApiArgs = {
get_nodes: "...",
get_nodes_status: "...",
list_path: "path, ...",
check_path: "path, ...",
execute_command: "job, cmd_string, user_token, ...",
is_command_done: "job, ...",
cleanup_job: "job, ...",
"actions.status": "job, action, params, ...",
"actions.terminate": "job, action, params, ...",
};
function replaceExpectCalls(method, chunk) {
return chunk.replace(
/(\s*)expect\((.+)\)(\s*;?)/g,
`$1expect($2, [[condition '$2' failed]], "${method}")$3`
);
}
class SgaDaemon {
constructor(config) {
constructor(config, driver = {}) {
this.config = config;
this.driver = driver;
}
async start() {
if (this.process) {
throw new Error("already started");
}
const mockupPath = path.join(testDir, "driver_mockup.lua");
const mockupFile = fs.createWriteStream(mockupPath);
mockupFile.write(`
local now, expect = ...
local driver = { actions = {} }
`);
const moduleBody = this.driver.__MODULE__;
if (moduleBody) {
mockupFile.write(moduleBody);
delete this.driver.__MODULE__;
}
for (let method in this.driver) {
mockupFile.write(`
function driver.${method}(self, ${driverApiArgs[method]})
${replaceExpectCalls(method, this.driver[method])}
end
`);
}
mockupFile.write("return driver");
mockupFile.end();
await events.once(mockupFile, "finish");
mockupFile.close();
const configPath = path.join(testDir, "sgad.cfg");
const configFile = fs.createWriteStream(configPath);
for (let property in this.config) {
......@@ -110,20 +155,30 @@ class SgaDaemon {
const luaBin = `${luaHome}/bin/lua`;
const luaLPath = `${luaHome}/share/lua/${luaVer}/?.lua;${luaHome}/share/lua/${luaVer}/?/init.lua`;
const luaCPath = `${luaHome}/lib/lua/${luaVer}/?.so`;
const luaSetup = `package.path='./lua_custom/?.lua;../?.lua;${luaLPath}'
package.cpath='${luaCPath}'`;
this.process = child_process.spawn(
luaBin,
["-e", luaSetup, "../sgad.lua", configPath],
{ stdio: ["ignore", sgaOutput, sgaOutput] }
);
this.process = child_process.spawn(luaBin, ["../sgad.lua", configPath], {
stdio: ["ignore", sgaOutput, sgaOutput],
env: {
LUA_PATH: `./lua_custom/?.lua;../?.lua;${luaLPath}`,
LUA_CPATH: luaCPath,
SGA_TESTDIR: testDir,
},
});
this.process.on("exit", (exitVal, signalName) => {
sgaDaemons.delete(this);
this.process = undefined;
this.config = undefined;
this.exitVal = exitVal;
this.signalName = signalName;
const failuresPath = path.join(testDir, "driver_failures.txt");
if (fs.existsSync(failuresPath)) {
const failuresMsgs = fs.readFileSync(failuresPath);
if (failuresMsgs.length > 0) {
fail(failuresMsgs.toString());
}
fs.unlinkSync(failuresPath);
}
});
sgaDaemons.add(this);
......@@ -228,24 +283,6 @@ class RegistryService {
}
register(req, res) {
const body = req.body;
expect(body.name).toBe(this.sgaConfig.sga_name);
expect(body.type).toBe("machine");
expect(body.platform).toBe(this.sgaConfig.platform);
expect(body.project_root_dir).toBe(this.sgaConfig.project_root_dir);
expect(body.algorithm_root_dir).toBe(this.sgaConfig.algorithm_root_dir);
expect(body.sandbox_root_dir).toBe(this.sgaConfig.sandbox_root_dir);
expect(body.actions.path).toBeString();
expect(body.actions.paths).toBeString();
expect(body.actions.job).toBeString();
expect(body.actions.shutdown).toBeString();
expect(body.resources).toEqual(
jasmine.objectContaining(this.sgaConfig.resources)
);
expect(body.extra_config).toEqual(
jasmine.objectContaining(this.sgaConfig.extra_config)
);
this.timestamps.heartbeat = process.hrtime.bigint();
res.status(201).json(this.regReply);
}
......@@ -280,6 +317,39 @@ async function clearAllResources() {
}
}
function checkRegistration(req, config) {
const body = req.body;
expect(body.name).toBe(config.sga_name);
expect(body.type).toBe("machine");
expect(body.platform).toBe(config.platform);
expect(body.project_root_dir).toBe(config.project_root_dir);
expect(body.algorithm_root_dir).toBe(config.algorithm_root_dir);
expect(body.sandbox_root_dir).toBe(config.sandbox_root_dir);
expect(body.actions.path).toBeString();
expect(body.actions.paths).toBeString();
expect(body.actions.job).toBeString();
expect(body.actions.shutdown).toBeString();
expect(body.resources).toEqual(jasmine.objectContaining(config.resources));
expect(body.extra_config).toEqual(
jasmine.objectContaining(config.extra_config)
);
}
function fillJobBody(id = "XPTO", params = {}, sandboxCount = 3) {
params.csbase_command_user_token = `job ${id} user token`;
params.csbase_command_path = `job ${id} path`;
params.csbase_command_output_path = `job ${id} output path`;
params.csbase_command_root_path = `job ${id} root path`;
for (let i = 0; i < sandboxCount; i++) {
params[`csbase_command_sandbox_paths.${i}`] = `job ${id} sandbox path ${i}`;
}
return {
cmd_string: `job ${id} command string`,
cmd_id: id,
parameters: params,
};
}
const waitMsec = (delay) =>
new Promise((resolve) => setTimeout(resolve, delay));
......@@ -288,3 +358,5 @@ exports.fillSgaConfig = fillSgaConfig;
exports.SgaDaemon = SgaDaemon;
exports.RegistryService = RegistryService;
exports.clearAllResources = clearAllResources;
exports.checkRegistration = checkRegistration;
exports.fillJobBody = fillJobBody;
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment