Skip to content

Commit

Permalink
Adds openapi v3.0.3 unit test spec, includes test cases, autogenerate…
Browse files Browse the repository at this point in the history
…s model tests with them (#12619)

* Adds draft6 tests and python file reader

* Adds processing of multiple files

* Moves test examples into a different component so component schemas can be booleans

* Excludes boolean schema cases and some others that contain patternProperties

* Adds automatic test generation, template not quite working with indentation

* Turns on allOf tests, some failing

* Adds more generated components and tests

* Adds enum tests

* Turns on all of themissing tests

* Adds exclmax and min exclusion reasons

* Adds items test cases

* Adds maximum

* Adds maxItems

* Adds maxLength

* Adds maxProperties

* Adds minimum

* Adds minItems

* Adds minLength

* Adds minProperties

* Adds multipleOf

* Adds not

* Adds oneOf

* Adds pattern

* Adds patternProperties

* Working on properties examples, partial fix for escaped characters

* Further improves example string escaping

* Fixes properties test cases

* Adds draft6 test samples license

* Adds ref

* Finishes ref

* Adds remoteRef

* Adds required

* Improves required testing

* Fixes build error / javadoc warning

* Fixes uniqueItems bug in python-experimental

* Turns all tests back on

* Fixes 2 failing tests, all python tests pass

* Fixes java npe errors

* Fixes formatting of tests, indentation fixed

* Test fase name fixed to toTestCaseName, docstring added to ObjectWithTypeBooleans

* Fixes typo

* Adds test deletion to samples generation, samples regenerated

* Updates python-exp unit test sample, includes new ref examples
  • Loading branch information
spacether authored Jul 1, 2022
1 parent 4cf58f5 commit 6158274
Show file tree
Hide file tree
Showing 302 changed files with 26,949 additions and 53 deletions.
1 change: 1 addition & 0 deletions CI/circle_parallel.sh
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ elif [ "$NODE_INDEX" = "4" ]; then
#mvn --no-snapshot-updates --quiet verify -Psamples.circleci.node4 -Dorg.slf4j.simpleLogger.defaultLogLevel=error
(cd samples/openapi3/client/petstore/python && make test)
(cd samples/openapi3/client/petstore/python-experimental && make test)
(cd samples/openapi3/client/3_0_3_unit_test/python-experimental && make test)

else
echo "Running node $NODE_INDEX to test 'samples.circleci.others' defined in pom.xml ..."
Expand Down
6 changes: 6 additions & 0 deletions bin/configs/python-experimental_3_0_3_unit_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
generatorName: python-experimental
outputDir: samples/openapi3/client/3_0_3_unit_test/python-experimental
inputSpec: modules/openapi-generator/src/test/resources/3_0/unit_test_spec/3_0_3_unit_test_spec.yaml
templateDir: modules/openapi-generator/src/main/resources/python-experimental
additionalProperties:
packageName: unit_test_api
2 changes: 2 additions & 0 deletions bin/generate-samples.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ else
echo "Please press CTRL+C to stop or the script will continue in 5 seconds."

sleep 5
# delete the 3_0_3 python-experimental tests because they are autogenerated our tooling needs to see differences
rm -rf "${root}/samples/openapi3/client/3_0_3_unit_test/python-experimental/test"

if [ ${#files[@]} -eq 0 ]; then
files=("${root}"/bin/configs/*.yaml)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ public class CodegenModel implements IJsonSchemaValidationProperties {
public Map<String, Object> vendorExtensions = new HashMap<>();
private CodegenComposedSchemas composedSchemas;
private boolean hasMultipleTypes = false;
public HashMap<String, SchemaTestCase> testCases = new HashMap<>();

/**
* The type of the value for the additionalProperties keyword in the OAS document.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2696,6 +2696,52 @@ protected void updateModelForAnyType(CodegenModel m, Schema schema) {
setAddProps(schema, m);
}

protected String toTestCaseName(String specTestCaseName) {
return specTestCaseName;
}

/**
* A method that allows generators to pre-process test example payloads
* This can be useful if one needs to change how values like null in string are represnted
* @param data the test data payload
* @return the updated test data payload
*/
protected Object processTestExampleData(Object data) {
return data;
}

/**
* Processes any test cases if they exist in the components.x-test-examples vendor extensions
* If they exist then cast them to java class instances and return them back in a map
* @param schemaName the component schema name that the test cases are for
* @param vendorExtensions the extensions that may or may not hold the data
*/
private HashMap<String, SchemaTestCase> extractSchemaTestCases(String schemaName, HashMap<String, Object> vendorExtensions) {
String testExamplesKey = "x-schema-test-examples";
// schemaName to a map of test case name to test case
if (vendorExtensions == null || !vendorExtensions.containsKey(testExamplesKey)) {
return null;
}
HashMap<String, SchemaTestCase> schemaTestCases = new HashMap<>();
LinkedHashMap<String, Object> schemaNameToTestCases = (LinkedHashMap<String, Object>) vendorExtensions.get(testExamplesKey);

if (!schemaNameToTestCases.containsKey(schemaName)) {
return null;
}
LinkedHashMap<String, LinkedHashMap<String, Object>> testNameToTesCase = (LinkedHashMap<String, LinkedHashMap<String, Object>>) schemaNameToTestCases.get(schemaName);
for (Entry<String, LinkedHashMap<String, Object>> entry: testNameToTesCase.entrySet()) {
LinkedHashMap<String, Object> testExample = (LinkedHashMap<String, Object>) entry.getValue();
String nameInSnakeCase = toTestCaseName(entry.getKey());
Object data = processTestExampleData(testExample.get("data"));
SchemaTestCase testCase = new SchemaTestCase(
(String) testExample.getOrDefault("description", ""),
new ObjectWithTypeBooleans(data),
(boolean) testExample.get("valid")
);
schemaTestCases.put(nameInSnakeCase, testCase);
}
return schemaTestCases;
}

/**
* Convert OAS Model object to Codegen Model object.
Expand All @@ -2721,6 +2767,11 @@ public CodegenModel fromModel(String name, Schema schema) {

CodegenModel m = CodegenModelFactory.newInstance(CodegenModelType.MODEL);
ModelUtils.syncValidationProperties(schema, m);
if (openAPI != null) {
HashMap<String, Object> vendorExtensions = (HashMap<String, Object>) openAPI.getComponents().getExtensions();
HashMap<String, SchemaTestCase> schemaTestCases = extractSchemaTestCases(name, vendorExtensions);
m.testCases = schemaTestCases;
}

if (reservedWords.contains(name)) {
m.name = escapeReservedWord(name);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.openapitools.codegen;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;

public class ObjectWithTypeBooleans {
public boolean isUnboundedInteger;
public boolean isNumber;
public boolean isString;
public boolean isMap;
public boolean isArray;
public boolean isBoolean;
public boolean isNull;
public Object value;

/**
* A wrapper class that is used to store payloads to be ingested by schemas
* This class includes the payload value in the value property
* Other booleans: isUnboundedInteger/isNumber/isString/isMap/isArray/isBoolean/isNull
* allow generator templates to decide how to render each payload into code
* based upon what type it is. The booleans isX describe the value in value.
* @param value the input payload that is stored
*/
public ObjectWithTypeBooleans(Object value) {
Object usedValue = null;
if (value instanceof Integer){
this.isUnboundedInteger = true;
this.value = value;
} else if (value instanceof Double || value instanceof Float){
this.isNumber = true;
this.value = value;
} else if (value instanceof String) {
this.isString = true;
this.value = value;
} else if (value instanceof LinkedHashMap) {
LinkedHashMap<String, Object> castValue = (LinkedHashMap<String, Object>) value;
LinkedHashMap<ObjectWithTypeBooleans, ObjectWithTypeBooleans> castMap = new LinkedHashMap<>();
for (Map.Entry entry: castValue.entrySet()) {
ObjectWithTypeBooleans entryKey = new ObjectWithTypeBooleans(entry.getKey());
ObjectWithTypeBooleans entryValue = new ObjectWithTypeBooleans(entry.getValue());
castMap.put(entryKey, entryValue);
}
this.value = castMap;
this.isMap = true;
} else if (value instanceof ArrayList) {
ArrayList<ObjectWithTypeBooleans> castList = new ArrayList<>();
for (Object item: (ArrayList<Object>) value) {
castList.add(new ObjectWithTypeBooleans(item));
}
this.value = castList;
this.isArray = true;
} else if (value instanceof Boolean) {
this.isBoolean = true;
this.value = value;
} else if (value == null) {
this.isNull = true;
this.value = value;
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.openapitools.codegen;

public class SchemaTestCase {
public String description;
public ObjectWithTypeBooleans data;
// true means the test case should pass, false means it should fail
public boolean valid;

public SchemaTestCase(String description, ObjectWithTypeBooleans data, boolean valid) {
this.description = description;
this.data = data;
this.valid = valid;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.github.curiousoddman.rgxgen.RgxGen;
import com.github.curiousoddman.rgxgen.config.RgxGenOption;
import com.github.curiousoddman.rgxgen.config.RgxGenProperties;
import com.google.common.base.CaseFormat;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.servers.Server;
Expand Down Expand Up @@ -848,6 +849,7 @@ public CodegenProperty fromProperty(String name, Schema p) {
// templates use its presence to handle these badly named variables / keys
if ((isReservedWord(name) || !isValidPythonVarOrClassName(name)) && !name.equals(cp.name)) {
cp.nameInSnakeCase = cp.name;
cp.baseName = (String) processTestExampleData(name);
} else {
cp.nameInSnakeCase = null;
}
Expand Down Expand Up @@ -1127,7 +1129,8 @@ public String toEnumValue(String value, String datatype) {
} else if ("bool".equals(datatype)) {
return value.substring(0, 1).toUpperCase(Locale.ROOT) + value.substring(1);
} else {
return ensureQuotes(value);
String fixedValue = (String) processTestExampleData(value);
return ensureQuotes(fixedValue);
}
}

Expand All @@ -1152,6 +1155,67 @@ protected void addParentContainer(CodegenModel model, String name, Schema schema
model.dataType = getTypeString(schema, "", "", referencedModelNames);
}

protected String toTestCaseName(String specTestCaseName) {
return CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, specTestCaseName);
}

protected Object processTestExampleData(Object value) {
if (value instanceof Integer){
return value;
} else if (value instanceof Double || value instanceof Float || value instanceof Boolean){
return value;
} else if (value instanceof String) {
String stringValue = (String) value;
String backslash = "\\";
if (stringValue.contains(backslash)) {
stringValue = stringValue.replace(backslash, "\\\\");
}
String nullChar = "\0";
if (stringValue.contains(nullChar)) {
stringValue = stringValue.replace(nullChar, "\\x00");
}
String doubleQuoteChar = "\"";
if (stringValue.contains(doubleQuoteChar)) {
stringValue = stringValue.replace(doubleQuoteChar, "\\\"");
}
String lineSep = System.lineSeparator();
if (stringValue.contains(lineSep)) {
stringValue = stringValue.replace(lineSep, "\\n");
}
String carriageReturn = "\r";
if (stringValue.contains(carriageReturn)) {
stringValue = stringValue.replace(carriageReturn, "\\r");
}
String tab = "\t";
if (stringValue.contains(tab)) {
stringValue = stringValue.replace(tab, "\\t");
}
String formFeed = "\f";
if (stringValue.contains(formFeed)) {
stringValue = stringValue.replace(formFeed, "\\f");
}
return stringValue;
} else if (value instanceof LinkedHashMap) {
LinkedHashMap<String, Object> fixedValues = new LinkedHashMap();
for (Map.Entry entry: ((LinkedHashMap<String, Object>) value).entrySet()) {
String entryKey = (String) processTestExampleData(entry.getKey());
Object entryValue = processTestExampleData(entry.getValue());
fixedValues.put(entryKey, entryValue);
}
return fixedValues;
} else if (value instanceof ArrayList) {
ArrayList<Object> fixedValues = (ArrayList<Object>) value;
for (int i = 0; i < fixedValues.size(); i++) {
Object item = processTestExampleData(fixedValues.get(i));
fixedValues.set(i, item);
}
return fixedValues;
} else if (value == null) {
return value;
}
return value;
}

/**
* Convert OAS Model object to Codegen Model object
* We have a custom version of this method so we can:
Expand Down Expand Up @@ -1388,12 +1452,6 @@ protected Object getObjectExample(Schema sc) {
* @return quoted string
*/
private String ensureQuotes(String in) {
Pattern pattern = Pattern.compile("\r\n|\r|\n");
Matcher matcher = pattern.matcher(in);
if (matcher.find()) {
// if a string has a new line in it add triple quotes to make it a python multiline string
return "'''" + in + "'''";
}
String strPattern = "^['\"].*?['\"]$";
if (in.matches(strPattern)) {
return in;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
,
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{{#if isMap}}
{
{{#each value}}
{{#with @key}}
{{> model_templates/payload_renderer endChar=':'}}
{{/with}}
{{#with this}}
{{> model_templates/payload_renderer endChar=','}}
{{/with}}
{{/each}}
}{{endChar}}
{{/if}}
{{#if isArray}}
[
{{#each value}}
{{> model_templates/payload_renderer endChar=','}}
{{/each}}
]{{endChar}}
{{/if}}
{{#or isNumber isUnboundedInteger}}
{{value}}{{endChar}}
{{/or}}
{{#if isBoolean}}
{{#if value}}
True{{endChar}}
{{else}}
False{{endChar}}
{{/if}}
{{/if}}
{{#if isNull}}
None{{endChar}}
{{/if}}
{{#if isString}}
"{{{value}}}"{{endChar}}
{{/if}}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
{{/or}}
{{/or}}
{{#if nameInSnakeCase}}
locals()['{{baseName}}'] = {{name}}
locals()["{{{baseName}}}"] = {{name}}
del locals()['{{name}}']
{{/if}}
{{/if}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ _SchemaValidator(
{{#if minItems}}
min_items={{minItems}},
{{/if}}
{{#if maxProperties}}
{{#neq maxProperties null }}
max_properties={{maxProperties}},
{{/if}}
{{/neq}}
{{#if minProperties}}
min_properties={{minProperties}},
{{/if}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,36 @@ import {{packageName}}
{{#each models}}
{{#with model}}
from {{packageName}}.{{modelPackage}}.{{classFilename}} import {{classname}}
from {{packageName}} import configuration


class Test{{classname}}(unittest.TestCase):
"""{{classname}} unit test stubs"""
_configuration = configuration.Configuration()

{{#each testCases}}
{{#with this }}
def test_{{@key}}_{{#if valid}}passes{{else}}fails{{/if}}(self):
# {{description}}
{{#if valid}}
{{classname}}._from_openapi_data(
{{#with data}}
{{> model_templates/payload_renderer endChar=',' }}
{{/with}}
_configuration=self._configuration
)
{{else}}
with self.assertRaises(({{packageName}}.ApiValueError, {{packageName}}.ApiTypeError)):
{{classname}}._from_openapi_data(
{{#with data}}
{{> model_templates/payload_renderer endChar=','}}
{{/with}}
_configuration=self._configuration
)
{{/if}}
{{/with}}

def setUp(self):
pass

def tearDown(self):
pass

def test_{{classname}}(self):
"""Test {{classname}}"""
# FIXME: construct object with mandatory attributes with example values
# model = {{classname}}() # noqa: E501
pass

{{/each}}
{{/with}}
{{/each}}

Expand Down
Loading

0 comments on commit 6158274

Please sign in to comment.