diff --git a/.gitignore b/.gitignore index f4b600d..794c4a8 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ node_modules # Optional REPL history .node_repl_history .idea/ +temp_locustfile.py diff --git a/README.md b/README.md index a71c2e7..3ec9ad8 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ __Full Usage:__ -m, --min minimum time, in milliseconds, that a simulated user will wait between executing each task (default: 1000). -x, --max maximum time, in milliseconds, that a simulated user will wait between executing each task (default: 3000). -H, --host The host attribute is a URL prefix (i.e. “http://google.com”) to the host that is to be loaded. - + -c, --client Use this flag to generate a client for your HTTP API or Website and leae the TaskSet empty ``` ## Custom Swagger/OAI fields diff --git a/index.js b/index.js index 68d3439..cf81114 100755 --- a/index.js +++ b/index.js @@ -14,14 +14,17 @@ program .option('-m, --min ', 'minimum time, in milliseconds, that a simulated user will wait between executing each task (default: 1000).', parseInt) .option('-x, --max ', 'maximum time, in milliseconds, that a simulated user will wait between executing each task (default: 3000).', parseInt) .option('-H, --host ', 'The host attribute is a URL prefix (i.e. “http://google.com”) to the host that is to be loaded.') + .option('-c, --client', 'Set this flag to generate an API client instead of a TaskSet that you can use to program flexible locust behaviours', false) .action(function(file, options) { cmdValue = 'convert'; var actualOptions = s2l.defaultOptions; + var generateClient = false; if (options.host) { actualOptions['host'] = options.host} if (options.min) { actualOptions['min_wait'] = options.min} if (options.max) { actualOptions['max_wait'] = options.max} + if(options.client) {generateClient=true} if (file.indexOf('{') == 0) { // piped in JSON string console.log(s2l.convertJSON(file, actualOptions)); @@ -31,7 +34,7 @@ program console.error(chalk.red("Error: File not found or not a valid swagger yaml/json file: "+file)); console.log(chalk.yellow("Hint: Check that file exsits and extension is either .json or .yaml")); } else { // good file - console.log(s2l.convertFile(file, actualOptions)); + console.log(s2l.convertFile(file, actualOptions, generateClient)); } }); diff --git a/lib/swagger2locust.js b/lib/swagger2locust.js index dfad8e0..c367f18 100644 --- a/lib/swagger2locust.js +++ b/lib/swagger2locust.js @@ -22,9 +22,12 @@ module.exports = { }, // Convert a file - convertFile : function(filename, options) { + convertFile : function(filename, options, generateClient=false) { var contents = fs.readFileSync(filename); ext = filename.split('.').pop().toLowerCase(); + + this.generateClient = generateClient; + if (ext == "yaml") { return this.convertYAML(contents, options); } else { @@ -86,22 +89,48 @@ module.exports = { locustfileHeader : function(options) { var header = "" header += "from locust import HttpLocust, TaskSet, task\n" + if (this.generateClient) header += "from locust.clients import HttpSession\n" + if (options["x-locust-import"]) { for (imp in options["x-locust-import"]) { header += "import "+options["x-locust-import"][imp]+"\n"; } } - header += "\n" - header += "class MyTaskSet(TaskSet):\n" + header += "\n\n"; + if(this.generateClient) + { + header += "class MyHttpClient(HttpSession):\n" + } + else { + header += "class MyTaskSet(TaskSet):\n" + } return header; }, // Outputs the locust class and the settings (host, min, max, etc). locustfileFooter : function(options) { - var footer = "" + var footer = "\n"; + + if(this.generateClient) + { + footer += "class MyTaskSet(TaskSet):\n"; + footer += " pass\n\n\n" + } + footer += "class MyLocust(HttpLocust):\n" footer += " task_set = MyTaskSet\n" + + if(this.generateClient) + { + //add our custom client + //see https://docs.locust.io/en/stable/testing-other-systems.html for reference on custom clients + footer += "\n" + footer += " def __init__(self):\n"; + footer += " super().__init__()\n"; + footer += " self.client = MyHttpClient(self.host)\n\n"; + } + for(currOpt in options){ if (currOpt == "host") { footer += " "+currOpt+' = "'+options[currOpt]+'"'+"\n" @@ -114,13 +143,23 @@ module.exports = { // Creates an HTTP task from swagger path object using specified method name HTTPLocustGenericRequestFromPath : function(method, pathName, pathObj, basePath, sharedDefinitions) { - var task = ""; - task += " @task\n"; - task += " def "+this.taskNameFromPath(pathName, method)+"(self):\n"; + // TODO: use resolvedPath.paramsDict to get default parameters if generating taskset, not client var resolvedPath = this.resolvedPathWithDefaults(pathName, pathObj, sharedDefinitions); + this.replaceReservedPythonNames(resolvedPath.paramsNames); + + var task = ""; + + if(!this.generateClient) task += " @task\n"; + + task += " def "+this.taskNameFromPath(pathName, method)+"(self"+ (resolvedPath.paramsNames.length > 0 ? ", "+ resolvedPath.paramsNames.join(', ') : "") +", **kwargs):\n"; + //avoid "undefined" in URIs if(!basePath) basePath = ""; - task += ' self.client.'+method+'("'+basePath+resolvedPath.path+'"'+(resolvedPath.paramsValues.length > 0 ? ('.format('+resolvedPath.paramsValues.join(',')+')') : '')+', name="'+pathName+'")'+"\n\n"; + + //actual request call + task += ' return self' + if(!this.generateClient)task+=".client" + task +='.'+method+'("'+basePath+resolvedPath.path+'"'+(resolvedPath.paramsNames.length > 0 ? ('.format('+resolvedPath.paramsNames.join(',')+')') : '')+', name="'+pathName+'", **kwargs)'+"\n\n"; return task; }, @@ -128,20 +167,33 @@ module.exports = { // Creates an HTTP Get task from swagger path object HTTPLocustGetFromPath : function(pathName, pathObj, basePath, sharedDefinitions) { - var task = ""; - task += " @task\n"; - task += " def "+this.taskNameFromPath(pathName, 'get')+"(self):\n"; + //TODO: probably need to eliminate this method in favor of HTTPLocustGenericRequestFromPath + // TODO: use resolvedPath.paramsDict to get default parameters if generating taskset, not client var resolvedPath = this.resolvedPathWithDefaults(pathName, pathObj, sharedDefinitions); + this.replaceReservedPythonNames(resolvedPath.paramsNames); + + var task = ""; + + if(!this.generateClient) task += " @task\n"; + + task += " def "+this.taskNameFromPath(pathName, 'get')+"(self"+ (resolvedPath.paramsNames.length > 0 ? ", "+ resolvedPath.paramsNames.join(', ') : "") +", **kwargs):\n"; + //avoid "undefined" in URIs if(!basePath) basePath = ""; - task += ' self.client.get("'+basePath+resolvedPath.path+'"'+(resolvedPath.paramsValues.length > 0 ? ('.format('+resolvedPath.paramsValues.join(',')+')') : '')+', name="'+pathName+'")'+"\n\n"; + + //actual request call + task += ' return self' + if(!this.generateClient)task+=".client" + task +='.get'+'("'+basePath+resolvedPath.path+'"'+(resolvedPath.paramsNames.length > 0 ? ('.format('+resolvedPath.paramsNames.join(',')+')') : '')+', name="'+pathName+'", **kwargs)'+"\n\n"; return task; }, // Normalizes paths to py method name (GET /path/to-some/{id} will become get_path_to_some_id) taskNameFromPath : function (path, method) { - return method+path.toString().replace(/[\/-]/g,"_").replace(/[\{}]/g, "") + let result = method+path.toString().replace(/[\/-]/g,"_").replace(/[\{}]/g, "") + //remove trailing underscore if generated + return result.endsWith("_") ? result.slice(0, -1): result; }, // Replaces Path-Parameters' holders in path with their default values. @@ -150,6 +202,7 @@ module.exports = { var resolvedPath = pathName; var requiredParamValues = []; var currPlaceholder = 0; + let paramsDict = {}; // If path has path params, replace them with their default values if (pathName.indexOf("{") != -1) { @@ -159,9 +212,11 @@ module.exports = { resolvedPath = resolvedPath.replace('{'+pathParamsNames[currParam]+'}', '{'+currPlaceholder+'}'); currPlaceholder += 1; requiredParamValues.push(this.getParamDefault(pathParamsNames[currParam], pathObj['parameters'], sharedDefinitions['parameters'])); + paramsDict[pathParamsNames[currParam]] = this.getParamDefault(pathParamsNames[currParam], pathObj['parameters'], sharedDefinitions['parameters'], false); } } + // Collect required Query String Parameters (and their defaults) var requiredQSParams = []; for (currPathParam in pathObj['parameters']){ @@ -178,6 +233,7 @@ module.exports = { requiredQSParams.push(actualParam['name']+'={'+currPlaceholder+'}'); currPlaceholder += 1; requiredParamValues.push(this.getParamDefault(actualParam['name'], pathObj['parameters'], sharedDefinitions['parameters'])); + paramsDict[actualParam['name']] = this.getParamDefault(actualParam['name'], pathObj['parameters'], sharedDefinitions['parameters'], false); } } @@ -186,11 +242,11 @@ module.exports = { resolvedPath += "?"+requiredQSParams.join("&") } - return {path: resolvedPath, paramsValues: requiredParamValues}; + return {path: resolvedPath, paramsNames: pathParamsNames ? pathParamsNames : [], paramsValues: requiredParamValues, paramsDict: paramsDict}; }, // Finds the default value of parameter by its name. - getParamDefault : function(paramName, pathParameters, sharedParameters) { + getParamDefault : function(paramName, pathParameters, sharedParameters, doubleQuoting=true) { for (currPathParam in pathParameters) { var pathParam = pathParameters[currPathParam]; @@ -204,10 +260,29 @@ module.exports = { if (pathParam['x-locust-value']) { return pathParam['x-locust-value']; } else { - return '"'+pathParam['default']+'"'; + if (doubleQuoting) { + return '"' + pathParam['default'] + '"'; + } + else + { + return pathParam['default'] == undefined ? "None" : pathParam['default']; + } } } } throw new Error("Couldn't find default value of path param "+paramName); + }, + + replaceReservedPythonNames : function (nameArray) { + + let reservedNames= ["id"]; + + for (let i = 0; i < nameArray.length; i++) { + if(reservedNames.includes(nameArray[i])) + { + nameArray[i]="object_" + nameArray[i] + } + } + } -} +};