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:
@@ -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
|
||||
)
|
||||
|
||||
# ==================================================================================================
|
||||
|
||||
48
libs/viewer/include/viewer/Automation.h
Normal file
48
libs/viewer/include/viewer/Automation.h
Normal 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
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
232
libs/viewer/src/Automation.cpp
Normal file
232
libs/viewer/src/Automation.cpp
Normal 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
|
||||
@@ -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
33
libs/viewer/src/parse.h
Normal 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
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user