add vctest (WIP)

This commit is contained in:
MihailRis 2024-12-07 15:49:23 +03:00
parent d9bd60f473
commit 5e8805f241
16 changed files with 439 additions and 52 deletions

View File

@ -41,6 +41,7 @@ jobs:
- name: Run tests
run: ctest --test-dir ${{github.workspace}}/build
- name: Run engine (headless)
timeout-minutes: 1
run: |
chmod +x ${{github.workspace}}/build/VoxelEngine
${{github.workspace}}/build/VoxelEngine --headless --dir ${{github.workspace}}/userdir

View File

@ -43,6 +43,7 @@ jobs:
run: |
chmod +x build/VoxelEngine
build/VoxelEngine --headless --dir userdir
timeout-minutes: 1
# - name: Create DMG
# run: |
# mkdir VoxelEngineDmgContent

View File

@ -38,6 +38,7 @@ jobs:
run: ctest --output-on-failure --test-dir build
- name: Run engine (headless)
run: build/Release/VoxelEngine.exe --headless --dir userdir
timeout-minutes: 1
# - name: Package for Windows
# run: |
# mkdir packaged

View File

@ -82,3 +82,5 @@ if (VOXELENGINE_BUILD_TESTS)
enable_testing()
add_subdirectory(test)
endif()
add_subdirectory(vctest)

1
dev/tests/example.lua Normal file
View File

@ -0,0 +1 @@
print("Hello from the example test!")

View File

@ -328,6 +328,43 @@ function __vc_on_world_quit()
_rules.clear()
end
local __vc_coroutines = {}
local __vc_next_coroutine = 1
local __vc_coroutine_error = nil
function __vc_start_coroutine(chunk)
local co = coroutine.create(function()
local status, err = pcall(chunk)
if not status then
__vc_coroutine_error = err
end
end)
local id = __vc_next_coroutine
__vc_next_coroutine = __vc_next_coroutine + 1
__vc_coroutines[id] = co
return id
end
function __vc_resume_coroutine(id)
local co = __vc_coroutines[id]
if co then
coroutine.resume(co)
if __vc_coroutine_error then
error(__vc_coroutine_error)
end
return coroutine.status(co) ~= "dead"
end
return false
end
function __vc_stop_coroutine(id)
local co = __vc_coroutines[id]
if co then
coroutine.close(co)
__vc_coroutines[id] = nil
end
end
assets = {}
assets.load_texture = core.__load_texture

View File

@ -36,6 +36,7 @@
#include "window/Events.hpp"
#include "window/input.hpp"
#include "window/Window.hpp"
#include "interfaces/Process.hpp"
#include <iostream>
#include <assert.h>
@ -178,12 +179,25 @@ void Engine::saveScreenshot() {
void Engine::run() {
if (params.headless) {
logger.info() << "nothing to do";
runTest();
} else {
mainloop();
}
}
void Engine::runTest() {
if (params.testFile.empty()) {
logger.info() << "nothing to do";
return;
}
logger.info() << "starting test " << params.testFile;
auto process = scripting::start_coroutine(params.testFile);
while (process->isActive()) {
process->update();
}
logger.info() << "test finished";
}
void Engine::mainloop() {
logger.info() << "starting menu screen";
setScreen(std::make_shared<MenuScreen>(this));

View File

@ -49,6 +49,7 @@ struct CoreParameters {
bool headless = false;
std::filesystem::path resFolder {"res"};
std::filesystem::path userFolder {"."};
std::filesystem::path testFile;
};
class Engine : public util::ObjectsKeeper {
@ -93,6 +94,8 @@ public:
/// Automatically sets MenuScreen
void mainloop();
void runTest();
/// @brief Called after assets loading when all engine systems are initialized
void onAssetsLoaded();

View File

@ -0,0 +1,12 @@
#pragma once
/// @brief Process interface.
class Process {
public:
virtual ~Process() {}
virtual bool isActive() const = 0;
virtual void update() = 0;
virtual void waitForEnd() = 0;
virtual void terminate() = 0;
};

View File

@ -25,6 +25,7 @@
#include "util/timeutil.hpp"
#include "voxels/Block.hpp"
#include "world/Level.hpp"
#include "interfaces/Process.hpp"
using namespace scripting;
@ -71,6 +72,59 @@ void scripting::initialize(Engine* engine) {
load_script(fs::path("classes.lua"), true);
}
class LuaCoroutine : public Process {
lua::State* L;
int id;
bool alive = true;
public:
LuaCoroutine(lua::State* L, int id) : L(L), id(id) {
}
bool isActive() const override {
return alive;
}
void update() override {
if (lua::getglobal(L, "__vc_resume_coroutine")) {
lua::pushinteger(L, id);
if (lua::call(L, 1)) {
alive = lua::toboolean(L, -1);
lua::pop(L);
}
}
}
void waitForEnd() override {
while (isActive()) {
update();
}
}
void terminate() override {
if (lua::getglobal(L, "__vc_stop_coroutine")) {
lua::pushinteger(L, id);
lua::pop(L, lua::call(L, 1));
}
}
};
std::unique_ptr<Process> scripting::start_coroutine(
const std::filesystem::path& script
) {
auto L = lua::get_main_state();
if (lua::getglobal(L, "__vc_start_coroutine")) {
auto source = files::read_string(script);
lua::loadbuffer(L, 0, source, script.filename().u8string());
if (lua::call(L, 1)) {
int id = lua::tointeger(L, -1);
lua::pop(L, 2);
return std::make_unique<LuaCoroutine>(L, id);
}
lua::pop(L);
}
return nullptr;
}
[[nodiscard]] scriptenv scripting::get_root_environment() {
return std::make_shared<int>(0);
}

View File

@ -11,8 +11,6 @@
#include "typedefs.hpp"
#include "scripting_functional.hpp"
namespace fs = std::filesystem;
class Engine;
class Content;
struct ContentPack;
@ -34,6 +32,7 @@ class Entity;
struct EntityDef;
class GeneratorScript;
struct GeneratorDef;
class Process;
namespace scripting {
extern Engine* engine;
@ -60,6 +59,10 @@ namespace scripting {
void process_post_runnables();
std::unique_ptr<Process> start_coroutine(
const std::filesystem::path& script
);
void on_world_load(LevelController* controller);
void on_world_tick();
void on_world_save();
@ -136,7 +139,7 @@ namespace scripting {
void load_content_script(
const scriptenv& env,
const std::string& prefix,
const fs::path& file,
const std::filesystem::path& file,
const std::string& fileName,
block_funcs_set& funcsset
);
@ -150,7 +153,7 @@ namespace scripting {
void load_content_script(
const scriptenv& env,
const std::string& prefix,
const fs::path& file,
const std::filesystem::path& file,
const std::string& fileName,
item_funcs_set& funcsset
);
@ -161,13 +164,13 @@ namespace scripting {
/// @param fileName script file path using the engine format
void load_entity_component(
const std::string& name,
const fs::path& file,
const std::filesystem::path& file,
const std::string& fileName
);
std::unique_ptr<GeneratorScript> load_generator(
const GeneratorDef& def,
const fs::path& file,
const std::filesystem::path& file,
const std::string& dirPath
);
@ -179,7 +182,7 @@ namespace scripting {
void load_world_script(
const scriptenv& env,
const std::string& packid,
const fs::path& file,
const std::filesystem::path& file,
const std::string& fileName,
world_funcs_set& funcsset
);
@ -193,7 +196,7 @@ namespace scripting {
void load_layout_script(
const scriptenv& env,
const std::string& prefix,
const fs::path& file,
const std::filesystem::path& file,
const std::string& fileName,
uidocscript& script
);

View File

@ -3,17 +3,23 @@
#include "util/command_line.hpp"
#include "debug/Logger.hpp"
#include <iostream>
#include <stdexcept>
static debug::Logger logger("main");
int main(int argc, char** argv) {
debug::Logger::init("latest.log");
CoreParameters coreParameters;
if (!parse_cmdline(argc, argv, coreParameters)) {
return EXIT_SUCCESS;
try {
if (!parse_cmdline(argc, argv, coreParameters)) {
return EXIT_SUCCESS;
}
} catch (const std::runtime_error& err) {
std::cerr << err.what() << std::endl;
return EXIT_FAILURE;
}
debug::Logger::init(coreParameters.userFolder.string()+"/latest.log");
platform::configure_encoding();
try {
Engine(std::move(coreParameters)).run();

37
src/util/ArgsReader.hpp Normal file
View File

@ -0,0 +1,37 @@
#pragma once
#include <string>
#include <stdexcept>
#include <cstring>
namespace util {
class ArgsReader {
const char* last = "";
char** argv;
int argc;
int pos = 0;
public:
ArgsReader(int argc, char** argv) : argv(argv), argc(argc) {
}
void skip() {
pos++;
}
bool hasNext() const {
return pos < argc && std::strlen(argv[pos]);
}
bool isKeywordArg() const {
return last[0] == '-';
}
std::string next() {
if (pos >= argc) {
throw std::runtime_error("unexpected end");
}
last = argv[pos];
return argv[pos++];
}
};
}

View File

@ -1,48 +1,16 @@
#include "command_line.hpp"
#include <cstring>
#include <filesystem>
#include <iostream>
#include <stdexcept>
#include <string>
#include "files/engine_paths.hpp"
#include "util/ArgsReader.hpp"
#include "engine.hpp"
namespace fs = std::filesystem;
class ArgsReader {
const char* last = "";
char** argv;
int argc;
int pos = 0;
public:
ArgsReader(int argc, char** argv) : argv(argv), argc(argc) {
}
void skip() {
pos++;
}
bool hasNext() const {
return pos < argc && strlen(argv[pos]);
}
bool isKeywordArg() const {
return last[0] == '-';
}
std::string next() {
if (pos >= argc) {
throw std::runtime_error("unexpected end");
}
last = argv[pos];
return argv[pos++];
}
};
static bool perform_keyword(
ArgsReader& reader, const std::string& keyword, CoreParameters& params
util::ArgsReader& reader, const std::string& keyword, CoreParameters& params
) {
if (keyword == "--res") {
auto token = reader.next();
@ -53,22 +21,25 @@ static bool perform_keyword(
} else if (keyword == "--help" || keyword == "-h") {
std::cout << "VoxelEngine command-line arguments:\n";
std::cout << " --help - show help\n";
std::cout << " --res [path] - set resources directory\n";
std::cout << " --dir [path] - set userfiles directory\n";
std::cout << " --res <path> - set resources directory\n";
std::cout << " --dir <path> - set userfiles directory\n";
std::cout << " --headless - run in headless mode\n";
std::cout << " --test <path> - test script file\n";
std::cout << std::endl;
return false;
} else if (keyword == "--headless") {
params.headless = true;
} else if (keyword == "--test") {
auto token = reader.next();
params.testFile = fs::u8path(token);
} else {
std::cerr << "unknown argument " << keyword << std::endl;
return false;
throw std::runtime_error("unknown argument " + keyword);
}
return true;
}
bool parse_cmdline(int argc, char** argv, CoreParameters& params) {
ArgsReader reader(argc, argv);
util::ArgsReader reader(argc, argv);
reader.skip();
while (reader.hasNext()) {
std::string token = reader.next();

31
vctest/CMakeLists.txt Normal file
View File

@ -0,0 +1,31 @@
project(vctest)
set(CMAKE_CXX_STANDARD 17)
file(GLOB_RECURSE SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
add_executable(${PROJECT_NAME} ${SOURCES})
if(MSVC)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()
if((CMAKE_BUILD_TYPE EQUAL "Release") OR (CMAKE_BUILD_TYPE EQUAL "RelWithDebInfo"))
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Release>:Release>")
target_compile_options(${PROJECT_NAME} PRIVATE /W4 /MT /O2)
else()
target_compile_options(${PROJECT_NAME} PRIVATE /W4)
endif()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
else()
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra
-Wformat-nonliteral -Wcast-align
-Wpointer-arith -Wundef
-Wwrite-strings -Wno-unused-parameter)
endif()
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -no-pie -lstdc++fs")
endif()
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../src ${CMAKE_DL_LIBS})

213
vctest/main.cpp Normal file
View File

@ -0,0 +1,213 @@
#include <chrono>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>
#include <vector>
#include "util/ArgsReader.hpp"
namespace fs = std::filesystem;
inline fs::path TESTING_DIR = fs::u8path(".vctest");
struct Config {
fs::path executable;
fs::path directory;
fs::path resDir {"res"};
fs::path workingDir {"."};
bool outputAlways = false;
};
static bool perform_keyword(
util::ArgsReader& reader, const std::string& keyword, Config& config
) {
if (keyword == "--help" || keyword == "-h") {
std::cout << "Options\n\n";
std::cout << " --help, -h = show help\n";
std::cout << " --exe <path>, -e <path> = VoxelCore executable path\n";
std::cout << " --tests <path>, -d <path> = tests directory path\n";
std::cout << " --res <path>, -r <path> = 'res' directory path\n";
std::cout << " --working-dir <path>, -w <path> = user directory path\n";
std::cout << " --output-always = always show tests output\n";
std::cout << std::endl;
return false;
} else if (keyword == "--exe" || keyword == "-e") {
config.executable = fs::path(reader.next());
} else if (keyword == "--tests" || keyword == "-d") {
config.directory = fs::path(reader.next());
} else if (keyword == "--res" || keyword == "-r") {
config.resDir = fs::path(reader.next());
} else if (keyword == "--user" || keyword == "-u") {
config.workingDir = fs::path(reader.next());
} else if (keyword == "--output-always") {
config.outputAlways = true;
} else {
std::cerr << "unknown argument " << keyword << std::endl;
return false;
}
return true;
}
static bool parse_cmdline(int argc, char** argv, Config& config) {
util::ArgsReader reader(argc, argv);
while (reader.hasNext()) {
std::string token = reader.next();
if (reader.isKeywordArg()) {
if (!perform_keyword(reader, token, config)) {
return false;
}
}
}
return true;
}
static bool check_dir(const fs::path& dir) {
if (!fs::is_directory(dir)) {
std::cerr << dir << " is not a directory" << std::endl;
return false;
}
return true;
}
static void print_separator(std::ostream& stream) {
for (int i = 0; i < 32; i++) {
stream << "=";
}
stream << "\n";
}
static bool check_config(const Config& config) {
if (!fs::exists(config.executable)) {
std::cerr << "file " << config.executable << " not found" << std::endl;
return true;
}
if (!check_dir(config.directory)) {
return true;
}
if (!check_dir(config.resDir)) {
return true;
}
if (!check_dir(config.workingDir)) {
return true;
}
return false;
}
static void dump_config(const Config& config) {
std::cout << "paths:\n";
std::cout << " VoxelCore executable = " << fs::canonical(config.executable).string() << "\n";
std::cout << " Tests directory = " << fs::canonical(config.directory).string() << "\n";
std::cout << " Resources directory = " << fs::canonical(config.resDir).string() << "\n";
std::cout << " Working directory = " << fs::canonical(config.workingDir).string();
std::cout << std::endl;
}
static void cleanup(const fs::path& workingDir) {
auto dir = workingDir / TESTING_DIR;
std::cout << "cleaning up " << dir << std::endl;
fs::remove_all(dir);
}
static void setup_working_dir(const fs::path& workingDir) {
auto dir = workingDir / TESTING_DIR;
std::cout << "setting up working directory " << dir << std::endl;
if (fs::is_directory(dir)) {
cleanup(workingDir);
}
fs::create_directories(dir);
}
static void display_test_output(const fs::path& path, std::ostream& stream) {
stream << "[OUTPUT]" << std::endl;
if (fs::exists(path)) {
std::ifstream t(path);
stream << t.rdbuf();
}
}
static bool run_test(const Config& config, const fs::path& path) {
using std::chrono::duration_cast;
using std::chrono::high_resolution_clock;
using std::chrono::milliseconds;
auto outputFile = config.workingDir / "output.txt";
auto name = path.stem();
std::stringstream ss;
ss << config.executable << " --headless";
ss << " --test " << path;
ss << " --res " << config.resDir;
ss << " --dir " << config.workingDir;
ss << " >" << (config.workingDir / "output.txt") << " 2>&1";
auto command = ss.str();
print_separator(std::cout);
std::cout << "executing test " << name << "\ncommand: " << command << std::endl;
auto start = high_resolution_clock::now();
int code = system(command.c_str());
auto testTime =
duration_cast<milliseconds>(high_resolution_clock::now() - start)
.count();
if (code) {
display_test_output(outputFile, std::cerr);
std::cerr << "[FAILED] " << name << " in " << testTime << " ms" << std::endl;
fs::remove(outputFile);
return false;
} else {
if (config.outputAlways) {
display_test_output(outputFile, std::cout);
}
std::cout << "[PASSED] " << name << " in " << testTime << " ms" << std::endl;
fs::remove(outputFile);
return true;
}
}
int main(int argc, char** argv) {
Config config;
try {
if (!parse_cmdline(argc, argv, config)) {
return 0;
}
} catch (const std::runtime_error& err) {
std::cerr << err.what() << std::endl;
throw;
}
if (check_config(config)) {
return 1;
}
dump_config(config);
std::vector<fs::path> tests;
std::cout << "scanning for tests" << std::endl;
for (const auto& entry : fs::directory_iterator(config.directory)) {
auto path = entry.path();
if (path.extension().string() != ".lua") {
std::cout << " " << entry.path() << " skipped" << std::endl;
continue;
}
std::cout << " " << entry.path() << " enqueued" << std::endl;
tests.push_back(path);
}
setup_working_dir(config.workingDir);
config.workingDir /= TESTING_DIR;
size_t passed = 0;
std::cout << "running " << tests.size() << " test(s)" << std::endl;
for (const auto& path : tests) {
passed += run_test(config, path);
}
print_separator(std::cout);
cleanup(config.workingDir);
std::cout << std::endl;
std::cout << passed << " test(s) passed, " << (tests.size() - passed)
<< " test(s) failed" << std::endl;
if (passed < tests.size()) {
return 1;
}
return 0;
}