Introduce a configurable test case generator.

This lets us generate long lists of settings permutations by writing a
simple JSON spec. For example, the following spec would generate six
`Settings` objects, all of which have SSAO enabled. See the unit test
for a larger example.

```
{
    "name": "viewopts",
    "base": { "view.ssao.enabled": true }
    "permute": {
        "view.dithering": ["NONE", "TEMPORAL"],
        "view.sampleCount": [1, 4, 8]
    }
}
```
This commit is contained in:
Philip Rideout
2020-09-29 16:10:21 -07:00
parent 44d4694c29
commit 094e7169cc
8 changed files with 361 additions and 16 deletions

View File

@@ -8,13 +8,15 @@ set(PUBLIC_HDR_DIR include)
# Sources and headers
# ==================================================================================================
set(PUBLIC_HDRS
include/viewer/SimpleViewer.h
include/viewer/Automation.h
include/viewer/Settings.h
include/viewer/SimpleViewer.h
)
set(SRCS
src/SimpleViewer.cpp
src/Automation.cpp
src/Settings.cpp
src/SimpleViewer.cpp
)
# ==================================================================================================

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by mIcable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef VIEWER_AUTOMATION_H
#define VIEWER_AUTOMATION_H
#include <string>
#include <vector>
#include <viewer/Settings.h>
namespace filament {
namespace viewer {
// Named list of settings permutations.
struct AutomationSpec {
std::string name;
std::vector<Settings> cases;
};
// Consumes a JSON string and produces a list of automation specs using a counting algorithm.
//
// Each top-level item in the JSON is an object with "name", "base" and "permute".
// The "base" object specifies a single set of changes to apply to default settings.
// The optional "permute" object specifies a cross product of changes to apply to the base.
// See the unit test for an example.
//
// - Returns true if successful.
// - This function writes warnings and error messages into the utils log.
bool generate(const char* jsonChunk, size_t size, std::vector<AutomationSpec>* out);
} // namespace viewer
} // namespace filament
#endif // VIEWER_AUTOMATION_H

View File

@@ -46,7 +46,6 @@ using ToneMapping = filament::ColorGrading::ToneMapping;
using VignetteOptions = filament::View::VignetteOptions;
// Reads the given JSON blob and updates the corresponding fields in the given Settings object.
//
// - The given JSON blob need not specify all settings.
// - Returns true if successful.
// - This function writes warnings and error messages into the utils log.

View File

@@ -37,7 +37,7 @@ namespace filament {
namespace viewer {
/**
* \class SimpleViewer SimpleViewer.h gltfio/SimpleViewer.h
* \class SimpleViewer SimpleViewer.h viewer/SimpleViewer.h
* \brief Manages the state for a simple glTF viewer with imgui controls and a tree view.
*
* This is a utility that can be used across multiple platforms, including web.

View File

@@ -0,0 +1,232 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by mIcable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#define JSMN_HEADER
#include <viewer/Automation.h>
#include "parse.h"
#include <assert.h>
#include <sstream>
#include <utils/Log.h>
using namespace utils;
using std::vector;
static const bool VERBOSE = false;
namespace filament {
namespace viewer {
static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, std::string* val) {
CHECK_TOKTYPE(tokens[i], JSMN_STRING);
*val = STR(tokens[i], jsonChunk);
return i + 1;
}
static int parseBaseSettings(jsmntok_t const* tokens, int i, const char* jsonChunk, Settings* out) {
CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
int size = tokens[i++].size;
for (int j = 0; j < size; ++j, i += 2) {
std::stringstream dk(STR(tokens[i], jsonChunk));
std::string token;
std::string prefix;
int depth = 0;
// Expand "foo.bar.baz" into "foo: { bar: { baz: "
while (getline(dk, token, '.')) {
prefix += "{ \"" + token + "\": ";
depth++;
}
std::string json = prefix + STR(tokens[i + 1], jsonChunk);
for (int d = 0; d < depth; d++) { json += " } "; }
if (VERBOSE) {
slog.i << " Base: " << json.c_str() << io::endl;
}
// Now that we have a complete JSON string, apply this property change.
readJson(json.c_str(), json.size(), out);
}
return i;
}
static int parsePermutationsSpec(jsmntok_t const* tokens, int i, const char* jsonChunk,
vector<vector<std::string>>* out) {
CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
int size = tokens[i++].size;
out->resize(size);
for (int j = 0; j < size; ++j) {
std::stringstream dk(STR(tokens[i], jsonChunk));
std::string token;
std::string prefix;
int depth = 0;
// Expand "foo.bar.baz" into "foo: { bar: { baz: "
while (getline(dk, token, '.')) {
prefix += "{ \"" + token + "\": ";
depth++;
}
++i;
// Build a complete JSON string for each requested property value.
const jsmntok_t valueArray = tokens[i++];
CHECK_TOKTYPE(valueArray, JSMN_ARRAY);
vector<std::string>& spec = (*out)[j];
spec.resize(valueArray.size);
for (int k = 0; k < valueArray.size; k++, i++) {
std::string json = prefix + STR(tokens[i], jsonChunk);
for (int d = 0; d < depth; d++) { json += " } "; }
spec[k] = json;
}
}
return i;
}
static int parseAutomationSpec(jsmntok_t const* tokens, int i, const char* jsonChunk,
AutomationSpec* out) {
CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
int size = tokens[i++].size;
Settings base;
vector<vector<std::string>> permute;
for (int j = 0; j < size; ++j) {
const jsmntok_t tok = tokens[i];
CHECK_KEY(tok);
if (0 == compare(tok, jsonChunk, "name")) {
i = parse(tokens, i + 1, jsonChunk, &out->name);
if (VERBOSE) {
slog.i << "Building spec [" << out->name << "]" << io::endl;
}
} else if (0 == compare(tok, jsonChunk, "base")) {
i = parseBaseSettings(tokens, i + 1, jsonChunk, &base);
} else if (0 == compare(tok, jsonChunk, "permute")) {
i = parsePermutationsSpec(tokens, i + 1, jsonChunk, &permute);
} else {
slog.w << "Invalid automation key: '" << STR(tok, jsonChunk) << "'" << io::endl;
i = parse(tokens, i + 1);
}
if (i < 0) {
slog.e << "Invalid automation value: '" << STR(tok, jsonChunk) << "'" << io::endl;
return i;
}
}
// Determine the number of permutations.
size_t caseCount = 1;
size_t propIndex = 0;
vector<vector<std::string>::const_iterator> iters(permute.size());
for (const auto& prop : permute) {
caseCount *= prop.size();
if (VERBOSE) {
for (const auto& s : prop) {
slog.i << " Perm: " << s.c_str() << io::endl;
}
}
iters[propIndex++] = prop.begin();
}
out->cases.resize(caseCount);
if (VERBOSE) {
slog.i << " Case count: " << caseCount << io::endl;
}
size_t caseIndex = 0;
while (true) {
// Append a copy of the current Settings object to the case list.
if (VERBOSE) {
slog.i << " Appending case " << caseIndex << io::endl;
}
out->cases[caseIndex++] = base;
// Leave early if there are no permutations.
if (iters.empty()) {
return i;
}
// Use a basic counting algorithm to generate the next test case.
// Bump the first digit, if it rolls back to 0 then bump the next digit, etc.
// In this case, the "digit" is an iterator into a vector of JSON strings.
propIndex = 0;
for (auto& iter : iters) {
const auto& prop = permute[propIndex++];
if (++iter != prop.end()) {
break;
}
iter = prop.begin();
// Check if all permutations have been generated.
if (propIndex == permute.size()) {
assert(caseIndex == out->cases.size());
return i;
}
}
// Apply changes to the settings object.
for (const auto& iter : iters) {
const std::string& jsonString = *iter;
if (VERBOSE) {
slog.i << " Applying " << jsonString.c_str() << io::endl;
}
if (!readJson(jsonString.c_str(), jsonString.size(), &base)) {
return -1;
}
}
}
return i;
}
static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk,
vector<AutomationSpec>* out) {
CHECK_TOKTYPE(tokens[i], JSMN_ARRAY);
int size = tokens[i++].size;
out->resize(size);
for (int j = 0; j < size && i >= 0; ++j) {
i = parseAutomationSpec(tokens, i, jsonChunk, &out->at(j));
}
return i;
}
bool generate(const char* jsonChunk, size_t size, vector<AutomationSpec>* out) {
jsmn_parser parser = { 0, 0, 0 };
int tokenCount = jsmn_parse(&parser, jsonChunk, size, nullptr, 0);
if (tokenCount <= 0) {
return false;
}
jsmntok_t* tokens = (jsmntok_t*) malloc(sizeof(jsmntok_t) * tokenCount);
assert(tokens);
jsmn_init(&parser);
tokenCount = jsmn_parse(&parser, jsonChunk, size, tokens, tokenCount);
if (tokenCount <= 0) {
free(tokens);
return false;
}
int i = parse(tokens, 0, jsonChunk, out);
free(tokens);
return i >= 0;
}
} // namespace viewer
} // namespace filament

View File

@@ -18,16 +18,12 @@
#include <utils/Log.h>
#include <sstream>
#include <string>
#include "parse.h"
#include <assert.h>
#include <jsmn.h>
#define CHECK_TOKTYPE(tok_, type_) if ((tok_).type != (type_)) { return -1; }
#define CHECK_KEY(tok_) if ((tok_).type != JSMN_STRING || (tok_).size == 0) { return -1; }
#define STR(tok, jsonChunk) std::string(jsonChunk + tok.start, tok.end - tok.start)
#include <sstream>
#include <string>
using namespace utils;
@@ -35,14 +31,14 @@ namespace filament {
namespace viewer {
// Compares a JSON string token against a C string.
static int compare(jsmntok_t tok, const char* jsonChunk, const char* str) {
int compare(jsmntok_t tok, const char* jsonChunk, const char* str) {
size_t slen = strlen(str);
size_t tlen = tok.end - tok.start;
return (slen == tlen) ? strncmp(jsonChunk + tok.start, str, slen) : 128;
}
// Skips over an unused token.
static int parse(jsmntok_t const* tokens, int i) {
int parse(jsmntok_t const* tokens, int i) {
int end = i + 1;
while (i < end) {
switch (tokens[i].type) {
@@ -573,7 +569,7 @@ static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, ViewSett
return i;
}
static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, Settings* out) {
int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, Settings* out) {
CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
int size = tokens[i++].size;
for (int j = 0; j < size; ++j) {

33
libs/viewer/src/parse.h Normal file
View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by mIcable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <jsmn.h>
namespace filament {
namespace viewer {
#define CHECK_TOKTYPE(tok_, type_) if ((tok_).type != (type_)) { return -1; }
#define CHECK_KEY(tok_) if ((tok_).type != JSMN_STRING || (tok_).size == 0) { return -1; }
#define STR(tok, jsonChunk) std::string(jsonChunk + tok.start, tok.end - tok.start)
struct Settings;
int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, Settings* out);
int parse(jsmntok_t const* tokens, int i);
int compare(jsmntok_t tok, const char* jsonChunk, const char* str);
} // namespace viewer
} // namespace filament

View File

@@ -14,12 +14,15 @@
* limitations under the License.
*/
#include <viewer/Automation.h>
#include <viewer/Settings.h>
#include <gtest/gtest.h>
using namespace filament::viewer;
using std::vector;
class ViewSettingsTest : public testing::Test {};
static const char* JSON_TEST_DEFAULTS = R"TXT(
@@ -127,6 +130,30 @@ static const char* JSON_TEST_DEFAULTS = R"TXT(
}
)TXT";
static const char* JSON_AUTOMATION_TEST = R"TXT([
{
"name": "ppoff",
"base": {
"view.postProcessingEnabled": false
}
},
{
"name": "viewopts",
"base": {
"view.postProcessingEnabled": true
}
"permute": {
"view.dithering": ["NONE", "TEMPORAL"],
"view.sampleCount": [1, 4],
"view.taa.enabled": [false, true],
"view.antiAliasing": ["FXAA", "NONE"],
"view.ssao.enabled": [false, true],
"view.bloom.enabled": [false, true]
}
}
]
)TXT";
TEST_F(ViewSettingsTest, JsonTestDefaults) {
Settings settings1 = {0};
ASSERT_TRUE(readJson(JSON_TEST_DEFAULTS, strlen(JSON_TEST_DEFAULTS), &settings1));
@@ -135,10 +162,18 @@ TEST_F(ViewSettingsTest, JsonTestDefaults) {
Settings settings2;
ASSERT_TRUE(readJson("{}", strlen("{}"), &settings2));
ASSERT_EQ(writeJson(settings1), writeJson(settings2));
ASSERT_FALSE(readJson("{ badly_formed }", strlen("{ badly_formed }"), &settings2));
Settings settings3;
ASSERT_EQ(writeJson(settings1), writeJson(settings3));
ASSERT_EQ(writeJson(settings2), writeJson(settings3));
}
TEST_F(ViewSettingsTest, AutomationSpec) {
vector<AutomationSpec> specs;
ASSERT_TRUE(generate(JSON_AUTOMATION_TEST, strlen(JSON_AUTOMATION_TEST), &specs));
ASSERT_EQ(specs.size(), 2);
ASSERT_EQ(specs[0].cases.size(), 1);
ASSERT_EQ(specs[1].cases.size(), 1 << 6);
}
int main(int argc, char** argv) {