Index: binaries/data/mods/public/maps/random/rmgen/RandomMapLogger.js =================================================================== --- binaries/data/mods/public/maps/random/rmgen/RandomMapLogger.js +++ binaries/data/mods/public/maps/random/rmgen/RandomMapLogger.js @@ -4,11 +4,13 @@ this.startTime = Engine.GetMicroseconds(); this.prefix = ""; // seems noisy - this.printDirectly( - this.prefix + - "Generating " + g_MapSettings.Name + - " of size " + g_MapSettings.Size + - " and " + getNumPlayers() + " players.\n"); + // Don't print in test cases + if (g_MapSettings.Name) + this.printDirectly( + this.prefix + + "Generating " + g_MapSettings.Name + + " of size " + g_MapSettings.Size + + " and " + (g_MapSettings.PlayerData.length - 1) + " players.\n"); } RandomMapLogger.prototype.printDirectly = function(string) Index: binaries/data/mods/public/maps/random/tests/test_TileClass.js =================================================================== --- /dev/null +++ binaries/data/mods/public/maps/random/tests/test_TileClass.js @@ -0,0 +1,92 @@ +Engine.LoadLibrary("rmgen"); + +var g_MapSettings = { "Size": 512 }; +var g_Map = new RandomMap(0, "blackness"); + +// Test that that it checks by value, not by reference +{ + let tileClass = new TileClass(2); + let reference1 = new Vector2D(1, 1); + let reference2 = new Vector2D(1.000, 1.0); + tileClass.add(reference1); + TS_ASSERT(tileClass.has(reference2)); +} + +// Test out-of-bounds +{ + let tileClass = new TileClass(32); + let absentPoints = [ + new Vector2D(0, 0), + new Vector2D(0, 1), + new Vector2D(1, 0), + new Vector2D(-1, -1), + new Vector2D(2048, 0), + new Vector2D(0, NaN), + new Vector2D(0, Infinity) + ]; + + absentPoints.forEach(point => { + TS_ASSERT(!tileClass.has(point)); + }); +} + +// Test multi-insertion +{ + let tileClass = new TileClass(32); + let point = new Vector2D(1, 1); + + tileClass.add(point); + tileClass.add(point); + TS_ASSERT_EQUALS(tileClass.countMembersInRadius(point, 0), 1); + + // Still one point remaining + tileClass.remove(point); + tileClass.remove(point); + TS_ASSERT(!tileClass.has(point)); +} + +// Test Multi-insertion removal +{ + let tileClass = new TileClass(32); + let point = new Vector2D(2, 7); + + tileClass.add(point); + tileClass.add(point); + tileClass.remove(point); + TS_ASSERT(tileClass.has(point)); + + tileClass.remove(point); + TS_ASSERT(!tileClass.has(point)); +} + +//Test Multi-insertion removal +{ + let tileClass = new TileClass(55); + let point = new Vector2D(5, 4); + + for (let i = 0; i < 50; ++i) + tileClass.add(point); + + tileClass.remove(point); + TS_ASSERT(tileClass.has(point)); + + tileClass.remove(point); + TS_ASSERT(tileClass.has(point)); +} + +// Test getters +{ + let tileClass = new TileClass(88); + let point = new Vector2D(5, 1); + tileClass.add(point); + + let point2 = new Vector2D(4, 9); + tileClass.add(point2); + + TS_ASSERT_EQUALS(tileClass.countMembersInRadius(point, 1), 1); + TS_ASSERT_EQUALS(tileClass.countMembersInRadius(point, 100), 2); + + TS_ASSERT_EQUALS(tileClass.countNonMembersInRadius(point, 1), 4); + TS_ASSERT_EQUALS(tileClass.countNonMembersInRadius(point, 2), 12); + TS_ASSERT_EQUALS(tileClass.countNonMembersInRadius(point, 3), 28); +} Index: source/graphics/MapGenerator.h =================================================================== --- source/graphics/MapGenerator.h +++ source/graphics/MapGenerator.h @@ -81,11 +81,13 @@ * - Initialize and constructor/destructor must be called from the main thread. * - ScriptInterface created and destroyed by thread * - StructuredClone used to return JS map data - JS:Values can't be used across threads/runtimes. + * + * In test-mode, threading is disabled. */ class CMapGeneratorWorker { public: - CMapGeneratorWorker(); + CMapGeneratorWorker(const bool testing, ScriptInterface* scriptInterface); ~CMapGeneratorWorker(); /** @@ -111,6 +113,11 @@ */ shared_ptr GetResults(); + /** + * Perform the map generation or test. + */ + bool Run(); + private: /** @@ -182,11 +189,6 @@ static void* RunThread(void* data); /** - * Perform the map generation. - */ - bool Run(); - - /** * Currently loaded script librarynames. */ std::set m_LoadedLibraries; @@ -227,6 +229,12 @@ CTemplateLoader m_TemplateLoader; /** + * Set to true to spawn a new thread for mapgeneration. + * Set to false to run the testscripts in the current thread. + */ + bool m_Testing; + + /** * Holds the mapgeneration thread identifier. */ pthread_t m_WorkerThread; Index: source/graphics/MapGenerator.cpp =================================================================== --- source/graphics/MapGenerator.cpp +++ source/graphics/MapGenerator.cpp @@ -57,7 +57,8 @@ return true; } -CMapGeneratorWorker::CMapGeneratorWorker() +CMapGeneratorWorker::CMapGeneratorWorker(const bool testing, ScriptInterface* scriptInterface) : + m_Testing(testing), m_ScriptInterface(scriptInterface) { // If something happens before we initialize, that's a failure m_Progress = -1; @@ -66,7 +67,8 @@ CMapGeneratorWorker::~CMapGeneratorWorker() { // Wait for thread to end - pthread_join(m_WorkerThread, NULL); + if (m_Testing) + pthread_join(m_WorkerThread, NULL); } void CMapGeneratorWorker::Initialize(const VfsPath& scriptFile, const std::string& settings) @@ -78,6 +80,10 @@ m_ScriptPath = scriptFile; m_Settings = settings; + // For tests, the Run function is called in the same thread + if (!m_Testing) + return; + // Launch the worker thread int ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this); ENSURE(ret == 0); @@ -105,6 +111,8 @@ self->m_Progress = -1; } + delete self->m_ScriptInterface; + // At this point the random map scripts are done running, so the thread has no further purpose // and can die. The data will be stored in m_MapData already if successful, or m_Progress // will contain an error value on failure. @@ -114,15 +122,6 @@ bool CMapGeneratorWorker::Run() { - // We must destroy the ScriptInterface in the same thread because the JSAPI requires that! - // Also we must not be in a request when calling the ScriptInterface destructor, so the autoFree object - // must be instantiated before the request (destructors are called in reverse order of instantiation) - struct AutoFree { - AutoFree(ScriptInterface* p) : m_p(p) {} - ~AutoFree() { SAFE_DELETE(m_p); } - ScriptInterface* m_p; - } autoFree(m_ScriptInterface); - JSContext* cx = m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); @@ -142,7 +141,8 @@ } // Prevent unintentional modifications to the settings object by random map scripts - if (!m_ScriptInterface->FreezeObject(settingsVal, true)) + // Allow modification in testscripts + if (m_Testing && !m_ScriptInterface->FreezeObject(settingsVal, true)) { LOGERROR("CMapGeneratorWorker::Run: Failed to deepfreeze settings"); return false; @@ -158,7 +158,8 @@ // Copy settings to global variable JS::RootedValue global(cx, m_ScriptInterface->GetGlobalObject()); - if (!m_ScriptInterface->SetProperty(global, "g_MapSettings", settingsVal, true, true)) + + if (!m_ScriptInterface->SetProperty(global, "g_MapSettings", settingsVal, m_Testing, true)) { LOGERROR("CMapGeneratorWorker::Run: Failed to define g_MapSettings"); return false; @@ -405,7 +406,7 @@ ////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////// -CMapGenerator::CMapGenerator() : m_Worker(new CMapGeneratorWorker()) +CMapGenerator::CMapGenerator() : m_Worker(new CMapGeneratorWorker(true, nullptr)) { } Index: source/graphics/tests/test_MapGenerator.h =================================================================== --- /dev/null +++ source/graphics/tests/test_MapGenerator.h @@ -0,0 +1,57 @@ +/* Copyright (C) 2019 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "simulation2/system/ComponentTest.h" + +#include "ps/Filesystem.h" +#include "graphics/MapGenerator.h" + +class TestMapGenerator : public CxxTest::TestSuite +{ +public: + void setUp() + { + g_VFS = CreateVfs(); + g_VFS->Mount(L"", DataDir()/"mods"/"mod", VFS_MOUNT_MUST_EXIST); + g_VFS->Mount(L"", DataDir()/"mods"/"public", VFS_MOUNT_MUST_EXIST, 1); // ignore directory-not-found errors + CXeromyces::Startup(); + } + + void tearDown() + { + CXeromyces::Terminate(); + g_VFS.reset(); + } + + void test_mapgen_scripts() + { + VfsPaths paths; + TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"maps/random/tests/", L"test_*.js", paths)); + + const std::string settings("{\"Seed\":0}"); + + for (const VfsPath& path : paths) + { + ScriptInterface scriptInterface("Engine", "MapGenerator", g_ScriptRuntime); + ScriptTestSetup(scriptInterface); + + CMapGeneratorWorker worker(false, &scriptInterface); + worker.Initialize(path, settings); + worker.Run(); + } + } +};