From 5e63d8301e4b979d3f422f2511de5e5234756d72 Mon Sep 17 00:00:00 2001 From: whitequark Date: Mon, 25 Jul 2016 19:37:48 +0000 Subject: [PATCH] Add a simple harness for automated, headless testing. This commit alters the build system substantially; it adds another platform, `headless`, that provides stubs in place of all GUI functions, and provides a library `solvespace_headless` alongside the main executable. To cut down build times, only the few files that have #if defined(HEADLESS) are built twice for the executable and the library; the rest is grouped into a new `solvespace_cad` library. It is not usable on its own and just serves for grouping. This commit also gates the tests behind a -DENABLE_TESTS=ON CMake option, ON by default (but suggested as OFF in the README so that people don't ever have to install cairo to build the executable.) The tests introduced in this commit are (so far) rudimentary, although functional, and they serve as a stepping point towards introducing coverage analysis. --- .gitattributes | 30 +- .gitignore | 2 + .travis/build-debian.sh | 6 +- CMakeLists.txt | 53 +-- README.md | 27 +- appveyor.yml | 5 +- src/CMakeLists.txt | 91 ++--- src/confscreen.cpp | 2 + src/draw.cpp | 2 + src/export.cpp | 6 +- src/file.cpp | 32 +- src/platform/headless.cpp | 273 +++++++++++++++ src/platform/unixutil.cpp | 10 + src/platform/w32util.cpp | 20 ++ src/render/render.h | 2 + src/render/rendercairo.cpp | 6 +- src/resource.cpp | 67 ++++ src/resource.h | 4 + src/solvespace.cpp | 2 + src/solvespace.h | 2 + src/textwin.cpp | 2 + test/CMakeLists.txt | 18 + test/harness.cpp | 320 ++++++++++++++++++ test/harness.h | 57 ++++ test/request/line_segment/line_segment.png | Bin 0 -> 4300 bytes .../line_segment/line_segment_v20.slvs | 278 +++++++++++++++ .../line_segment/line_segment_v21.slvs | 278 +++++++++++++++ test/request/line_segment/test.cpp | 17 + 28 files changed, 1482 insertions(+), 130 deletions(-) create mode 100644 src/platform/headless.cpp create mode 100644 test/CMakeLists.txt create mode 100644 test/harness.cpp create mode 100644 test/harness.h create mode 100644 test/request/line_segment/line_segment.png create mode 100644 test/request/line_segment/line_segment_v20.slvs create mode 100644 test/request/line_segment/line_segment_v21.slvs create mode 100644 test/request/line_segment/test.cpp diff --git a/.gitattributes b/.gitattributes index 7c97503a..1f15ca3d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,22 +1,10 @@ -# .gitattributes for SolveSpace - -# Set default behaviour, in case users don't have core.autocrlf set. * text=auto - -# Explicitly declare text files we want to always be normalized and converted -# to native line endings on checkout. -*.cpp text -*.h text -*.txt text - -# Declare files that will always have CRLF line endings on checkout. -*.sln text eol=crlf - -# Denote all files that are truly binary and should not be modified. -*.gz binary -*.ico binary -*.jpg binary -*.lib binary -*.png binary - -# end .gitattributes +*.cpp text +*.h text +*.txt text +*.gz binary +*.ico binary +*.jpg binary +*.lib binary +*.png binary +*.slvs binary diff --git a/.gitignore b/.gitignore index 28be69f1..dd7a50b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ /CMakeCache.txt /build*/ +/test/**/*.diff.* +/test/**/*.curr.* *.trace # OpenGL apitrace files /debian/tmp/ /debian/*.log diff --git a/.travis/build-debian.sh b/.travis/build-debian.sh index 124e45ba..f15b8846 100755 --- a/.travis/build-debian.sh +++ b/.travis/build-debian.sh @@ -2,5 +2,7 @@ if echo $TRAVIS_TAG | grep ^v; then BUILD_TYPE=RelWithDebInfo; else BUILD_TYPE=Debug; fi -export BUILD_TYPE -dpkg-buildpackage -b -us -uc +mkdir build +cd build +cmake -DCMAKE_C_COMPILER=gcc-5 -DCMAKE_CXX_COMPILER=g++-5 -DCMAKE_BUILD_TYPE=$BUILD_TYPE .. +make VERBOSE=1 diff --git a/CMakeLists.txt b/CMakeLists.txt index 58ba9c8d..84a29b7a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,8 @@ set(solvespace_VERSION_MAJOR 3) set(solvespace_VERSION_MINOR 0) string(SUBSTRING "${GIT_COMMIT_HASH}" 0 8 solvespace_GIT_HASH) +set(ENABLE_TESTS ON CACHE BOOL "Whether the test suite will be built and run") + if(NOT WIN32 AND NOT APPLE) set(GUI gtk2 CACHE STRING "GUI toolkit to use (one of: gtk2 gtk3)") endif() @@ -93,19 +95,21 @@ if(WIN32) PNG_PNG_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/extlib/libpng) list(APPEND PNG_PNG_INCLUDE_DIR ${CMAKE_BINARY_DIR}/extlib/libpng) - message(STATUS "Using in-tree pixman") - add_vendored_subdirectory(extlib/pixman) - set(PIXMAN_FOUND YES) - set(PIXMAN_LIBRARY pixman) - set(PIXMAN_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/pixman/pixman) - list(APPEND PIXMAN_INCLUDE_DIRS ${CMAKE_BINARY_DIR}/extlib/pixman/pixman) + if(ENABLE_TESTS) + message(STATUS "Using in-tree pixman") + add_vendored_subdirectory(extlib/pixman) + set(PIXMAN_FOUND YES) + set(PIXMAN_LIBRARY pixman) + set(PIXMAN_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/pixman/pixman) + list(APPEND PIXMAN_INCLUDE_DIRS ${CMAKE_BINARY_DIR}/extlib/pixman/pixman) - message(STATUS "Using in-tree cairo") - add_vendored_subdirectory(extlib/cairo) - set(CAIRO_FOUND YES) - set(CAIRO_LIBRARY cairo) - set(CAIRO_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/cairo/src) - list(APPEND CAIRO_INCLUDE_DIRS ${CMAKE_BINARY_DIR}/extlib/cairo/src) + message(STATUS "Using in-tree cairo") + add_vendored_subdirectory(extlib/cairo) + set(CAIRO_FOUND YES) + set(CAIRO_LIBRARIES cairo) + set(CAIRO_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/cairo/src) + list(APPEND CAIRO_INCLUDE_DIRS ${CMAKE_BINARY_DIR}/extlib/cairo/src) + endif() if(NOT MINGW) message(STATUS "Using prebuilt SpaceWare") @@ -122,10 +126,15 @@ elseif(APPLE) find_package(PNG REQUIRED) find_package(Freetype REQUIRED) + if(ENABLE_TESTS) + find_library(CAIRO_LIBRARIES cairo REQUIRED) + find_path(CAIRO_INCLUDE_DIRS cairo.h PATH_SUFFIXES cairo) + endif() + find_library(APPKIT_LIBRARY AppKit REQUIRED) - find_library(CAIRO_LIBRARY cairo REQUIRED) - find_path(CAIRO_INCLUDE_DIRS cairo.h PATH_SUFFIXES cairo) else() # Linux and compatible systems + find_package(PkgConfig REQUIRED) + find_package(Backtrace) find_package(SpaceWare) @@ -133,12 +142,13 @@ else() # Linux and compatible systems find_package(PNG REQUIRED) find_package(Freetype REQUIRED) - # Use freedesktop's pkg-config to locate everything else. - find_package(PkgConfig REQUIRED) + if(ENABLE_TESTS) + pkg_check_modules(CAIRO REQUIRED cairo) + endif() + pkg_check_modules(FONTCONFIG REQUIRED fontconfig) pkg_check_modules(JSONC REQUIRED json-c) pkg_check_modules(FREETYPE REQUIRED freetype2) - pkg_check_modules(CAIRO REQUIRED cairo) set(HAVE_GTK TRUE) if(GUI STREQUAL "gtk3") @@ -192,15 +202,18 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang set(WARNING_FLAGS "${WARNING_FLAGS} -Werror=switch") endif() +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WARNING_FLAGS}") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WARNING_FLAGS}") + if(WIN32) set(CMAKE_RC_FLAGS "${CMAKE_RC_FLAGS} -l0") endif() -set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WARNING_FLAGS}") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WARNING_FLAGS}") - # components add_subdirectory(res) add_subdirectory(src) add_subdirectory(exposed) +if(ENABLE_TESTS) + add_subdirectory(test) +endif() diff --git a/README.md b/README.md index 42a1b127..6441b9ff 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,12 @@ Building on Linux ### Building for Linux You will need CMake, libpng, zlib, json-c, fontconfig, freetype, gtkmm 2.4, -pangomm 1.4, OpenGL and OpenGL GLU. +pangomm 1.4, OpenGL and OpenGL GLU. To build tests, you will need cairo. On a Debian derivative (e.g. Ubuntu) these can be installed with: apt-get install libpng-dev libjson-c-dev libfreetype6-dev \ libfontconfig1-dev libgtkmm-2.4-dev libpangomm-1.4-dev \ - libgl-dev libglu-dev cmake + libcairo2-dev libgl-dev libglu-dev cmake Before building, check out the necessary submodules: @@ -41,7 +41,7 @@ After that, build SolveSpace as following: mkdir build cd build - cmake .. + cmake .. -DENABLE_TESTS=OFF make sudo make install @@ -63,15 +63,17 @@ After that, build 32-bit SolveSpace as following: mkdir build cd build - cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/Toolchain-mingw32.cmake .. - make solvespace + cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/Toolchain-mingw32.cmake \ + -DENABLE_TESTS=OFF + make Or, build 64-bit SolveSpace as following: mkdir build cd build - cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/Toolchain-mingw64.cmake .. - make solvespace + cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/Toolchain-mingw64.cmake \ + -DENABLE_TESTS=OFF + make The application is built as `build/src/solvespace.exe`. @@ -80,10 +82,11 @@ Space Navigator support will not be available. Building on Mac OS X -------------------- -You will need XCode tools, CMake, libpng and Freetype. Assuming you use +You will need XCode tools, CMake, libpng and Freetype. To build tests, you +will need cairo. Assuming you use [homebrew][], these can be installed with: - brew install cmake libpng freetype + brew install cmake libpng freetype cairo XCode has to be installed via AppStore; it requires a free Apple ID. @@ -95,7 +98,7 @@ After that, build SolveSpace as following: mkdir build cd build - cmake .. + cmake .. -DENABLE_TESTS=OFF make The app bundle is built in `build/src/solvespace.app`. @@ -123,7 +126,7 @@ Visual Studio install. Then, run the following in cmd or PowerShell: git submodule update --init mkdir build cd build - cmake .. -G "NMake Makefiles" + cmake .. -G "NMake Makefiles" -DENABLE_TESTS=OFF nmake ### MSVC build @@ -137,7 +140,7 @@ in bash: git submodule update --init mkdir build cd build - cmake .. + cmake .. -DENABLE_TESTS=OFF make [cmakewin]: http://www.cmake.org/download/#latest diff --git a/appveyor.yml b/appveyor.yml index 8078a565..3296c071 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,9 +6,12 @@ before_build: - cd build - set tag=x%APPVEYOR_REPO_TAG_NAME% - if %tag:~,2% == xv (set BUILD_TYPE=RelWithDebInfo) else (set BUILD_TYPE=Debug) - - cmake -G"Visual Studio 12" -T v120_xp -DCMAKE_BUILD_TYPE=%BUILD_TYPE% .. + - cmake -G"Visual Studio 12" -T v120_xp .. build_script: - msbuild "src\solvespace.vcxproj" /verbosity:minimal /property:Configuration=%BUILD_TYPE% /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" + - msbuild "test\solvespace_testsuite.vcxproj" /verbosity:minimal /property:Configuration=%BUILD_TYPE% /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" +test_script: + - test\%BUILD_TYPE%\solvespace_testsuite.exe artifacts: - path: build\src\Debug\solvespace.exe name: solvespace.exe diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 209ce15d..ab49c007 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -72,7 +72,8 @@ endif() if(WIN32) set(platform_SOURCES - platform/w32main.cpp) + platform/w32main.cpp + render/rendergl1.cpp) set(platform_LIBRARIES comctl32 @@ -83,7 +84,8 @@ elseif(APPLE) set(platform_SOURCES platform/cocoamain.mm - render/rendergl.cpp) + render/rendergl.cpp + render/rendergl1.cpp) set(platform_BUNDLED_LIBS ${PNG_LIBRARIES} @@ -94,10 +96,10 @@ elseif(APPLE) elseif(HAVE_GTK) set(platform_SOURCES platform/gtkmain.cpp - render/rendergl.cpp) + render/rendergl.cpp + render/rendergl1.cpp) set(platform_LIBRARIES - ${Backtrace_LIBRARIES} ${SPACEWARE_LIBRARIES}) foreach(pkg_config_lib GTKMM JSONC FONTCONFIG) @@ -107,9 +109,9 @@ elseif(HAVE_GTK) endforeach() endif() -# solvespace executable +# solvespace library -set(solvespace_HEADERS +set(solvespace_cad_HEADERS config.h dsc.h expr.h @@ -120,18 +122,15 @@ set(solvespace_HEADERS render/render.h srf/surface.h) -set(solvespace_SOURCES +set(solvespace_cad_SOURCES bsp.cpp clipboard.cpp - confscreen.cpp constraint.cpp constrainteq.cpp describescreen.cpp - draw.cpp drawconstraint.cpp drawentity.cpp entity.cpp - export.cpp exportstep.cpp exportvector.cpp expr.cpp @@ -147,20 +146,16 @@ set(solvespace_SOURCES polygon.cpp resource.cpp request.cpp - solvespace.cpp style.cpp system.cpp textscreens.cpp - textwin.cpp toolbar.cpp ttf.cpp undoredo.cpp util.cpp view.cpp render/render.cpp - render/rendergl1.cpp render/render2d.cpp - render/rendercairo.cpp srf/boolean.cpp srf/curve.cpp srf/merge.cpp @@ -170,25 +165,38 @@ set(solvespace_SOURCES srf/surfinter.cpp srf/triangulate.cpp) -add_executable(solvespace WIN32 MACOSX_BUNDLE - ${libslvs_HEADERS} - ${libslvs_SOURCES} +set(solvespace_cad_gl_SOURCES + confscreen.cpp + draw.cpp + export.cpp + solvespace.cpp + textwin.cpp) + +add_library(solvespace_cad STATIC ${util_SOURCES} + ${solvespace_cad_HEADERS} + ${solvespace_cad_SOURCES}) + +target_link_libraries(solvespace_cad + dxfrw + ${ZLIB_LIBRARY} + ${PNG_LIBRARY} + ${FREETYPE_LIBRARY} + ${Backtrace_LIBRARIES}) + +# solvespace gui executable + +add_executable(solvespace WIN32 MACOSX_BUNDLE + ${solvespace_cad_gl_SOURCES} ${platform_SOURCES} - ${solvespace_HEADERS} - ${solvespace_SOURCES} $) add_dependencies(solvespace resources) target_link_libraries(solvespace - dxfrw + solvespace_cad ${OPENGL_LIBRARIES} - ${ZLIB_LIBRARY} - ${PNG_LIBRARY} - ${FREETYPE_LIBRARY} - ${CAIRO_LIBRARY} ${platform_LIBRARIES}) if(WIN32 AND NOT MINGW) @@ -229,21 +237,22 @@ if(NOT WIN32) BUNDLE DESTINATION .) endif() -# valgrind +# solvespace headless library -add_custom_target(solvespace-valgrind - valgrind - --tool=memcheck - --verbose - --track-fds=yes - --log-file=vg.%p.out - --num-callers=50 - --error-limit=no - --read-var-info=yes - --leak-check=full - --leak-resolution=high - --show-reachable=yes - --track-origins=yes - --malloc-fill=0xac - --free-fill=0xde - $) +set(headless_SOURCES + platform/headless.cpp + render/rendercairo.cpp) + +add_library(solvespace_headless STATIC EXCLUDE_FROM_ALL + ${solvespace_cad_gl_SOURCES} + ${headless_SOURCES}) + +target_compile_definitions(solvespace_headless + PRIVATE -DHEADLESS) + +target_include_directories(solvespace_headless + INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(solvespace_headless + solvespace_cad + ${CAIRO_LIBRARIES}) diff --git a/src/confscreen.cpp b/src/confscreen.cpp index 3b66a6aa..08568563 100644 --- a/src/confscreen.cpp +++ b/src/confscreen.cpp @@ -302,12 +302,14 @@ void TextWindow::ShowConfiguration() { Printf(false, "%Ba %d %Fl%Ll%f[change]%E", SS.autosaveInterval, &ScreenChangeAutosaveInterval); +#if !defined(HEADLESS) const char *gl_vendor, *gl_renderer, *gl_version; OpenGl1Renderer::GetIdent(&gl_vendor, &gl_renderer, &gl_version); Printf(false, ""); Printf(false, " %Ftgl vendor %E%s", gl_vendor); Printf(false, " %Ft renderer %E%s", gl_renderer); Printf(false, " %Ft version %E%s", gl_version); +#endif } bool TextWindow::EditControlDoneForConfiguration(const char *s) { diff --git a/src/draw.cpp b/src/draw.cpp index dc0021f4..40a58eca 100644 --- a/src/draw.cpp +++ b/src/draw.cpp @@ -710,6 +710,7 @@ void GraphicsWindow::Draw(Canvas *canvas) { } void GraphicsWindow::Paint() { +#if !defined(HEADLESS) havePainted = true; auto renderStartTime = std::chrono::high_resolution_clock::now(); @@ -786,4 +787,5 @@ void GraphicsWindow::Paint() { } canvas.EndFrame(); +#endif } diff --git a/src/export.cpp b/src/export.cpp index c9e75852..e16ce3b5 100644 --- a/src/export.cpp +++ b/src/export.cpp @@ -1086,11 +1086,12 @@ void SolveSpaceUI::ExportMeshAsThreeJsTo(FILE *f, const std::string &filename, // rendering the view in the usual way and then copying the pixels. //----------------------------------------------------------------------------- void SolveSpaceUI::ExportAsPngTo(const std::string &filename) { +#if !defined(HEADLESS) // No guarantee that the back buffer contains anything valid right now, // so repaint the scene. And hide the toolbar too. bool prevShowToolbar = SS.showToolbar; SS.showToolbar = false; -#ifndef WIN32 +#if !defined(WIN32) GlOffscreen offscreen; offscreen.Render((int)SS.GW.width, (int)SS.GW.height, [&] { SS.GW.Paint(); @@ -1109,10 +1110,11 @@ void SolveSpaceUI::ExportAsPngTo(const std::string &filename) { } if(f) fclose(f); -#ifndef WIN32 +#if !defined(WIN32) offscreen.Clear(); #endif return; +#endif } diff --git a/src/file.cpp b/src/file.cpp index 05f1d92c..4b199d19 100644 --- a/src/file.cpp +++ b/src/file.cpp @@ -734,34 +734,6 @@ static void PathSepNormalize(std::string &filename) } } -static std::string PathSepPlatformToUNIX(const std::string &filename) -{ -#if defined(WIN32) - std::string result = filename; - for(size_t i = 0; i < result.length(); i++) { - if(result[i] == '\\') - result[i] = '/'; - } - return result; -#else - return filename; -#endif -} - -static std::string PathSepUNIXToPlatform(const std::string &filename) -{ -#if defined(WIN32) - std::string result = filename; - for(size_t i = 0; i < result.length(); i++) { - if(result[i] == '/') - result[i] = '\\'; - } - return result; -#else - return filename; -#endif -} - bool SolveSpaceUI::ReloadAllImported(bool canCancel) { std::map linkMap; @@ -791,7 +763,7 @@ bool SolveSpaceUI::ReloadAllImported(bool canCancel) // In a newly created group we only have an absolute path. if(!g->linkFileRel.empty()) { - std::string rel = PathSepUNIXToPlatform(g->linkFileRel); + std::string rel = PathSepUnixToPlatform(g->linkFileRel); std::string fromRel = MakePathAbsolute(SS.saveFile, rel); FILE *test = ssfopen(fromRel, "rb"); if(test) { @@ -812,7 +784,7 @@ try_load_file: // Record the linked file's name relative to our filename; // if the entire tree moves, then everything will still work std::string rel = MakePathRelative(SS.saveFile, g->linkFile); - g->linkFileRel = PathSepPlatformToUNIX(rel); + g->linkFileRel = PathSepPlatformToUnix(rel); } else { // We're not yet saved, so can't make it absolute. // This will only be used for display purposes, as SS.saveFile diff --git a/src/platform/headless.cpp b/src/platform/headless.cpp new file mode 100644 index 00000000..036e3144 --- /dev/null +++ b/src/platform/headless.cpp @@ -0,0 +1,273 @@ +//----------------------------------------------------------------------------- +// Our main() function for the headless (no OpenGL) test runner. +// +// Copyright 2016 whitequark +//----------------------------------------------------------------------------- +#include "solvespace.h" +#include + +namespace SolveSpace { + +//----------------------------------------------------------------------------- +// Settings +//----------------------------------------------------------------------------- + +class Setting { +public: + enum class Type { + Undefined, + Int, + Float, + String + }; + + Type type; + int valueInt; + float valueFloat; + std::string valueString; + + void CheckType(Type expectedType) { + ssassert(type == Setting::Type::Undefined || + type == expectedType, "Wrong setting type"); + type = expectedType; + } +}; + +std::map settings; + +void CnfFreezeInt(uint32_t val, const std::string &key) { + Setting &setting = settings[key]; + setting.CheckType(Setting::Type::Int); + setting.valueInt = val; +} +uint32_t CnfThawInt(uint32_t val, const std::string &key) { + if(settings.find(key) != settings.end()) { + Setting &setting = settings[key]; + setting.CheckType(Setting::Type::Int); + val = setting.valueInt; + } + return val; +} + +void CnfFreezeFloat(float val, const std::string &key) { + Setting &setting = settings[key]; + setting.CheckType(Setting::Type::Float); + setting.valueFloat = val; +} +float CnfThawFloat(float val, const std::string &key) { + if(settings.find(key) != settings.end()) { + Setting &setting = settings[key]; + setting.CheckType(Setting::Type::Float); + val = setting.valueFloat; + } + return val; +} + +void CnfFreezeString(const std::string &val, const std::string &key) { + Setting &setting = settings[key]; + setting.CheckType(Setting::Type::String); + setting.valueString = val; +} +std::string CnfThawString(const std::string &val, const std::string &key) { + std::string ret = val; + if(settings.find(key) != settings.end()) { + Setting &setting = settings[key]; + setting.CheckType(Setting::Type::String); + ret = setting.valueString; + } + return ret; +} + +//----------------------------------------------------------------------------- +// Timers +//----------------------------------------------------------------------------- + +void SetTimerFor(int milliseconds) { +} +void SetAutosaveTimerFor(int minutes) { +} +void ScheduleLater() { +} + +//----------------------------------------------------------------------------- +// Graphics window +//----------------------------------------------------------------------------- + +void GetGraphicsWindowSize(int *w, int *h) { + *w = *h = 600; +} + +void InvalidateGraphics() { +} + +std::shared_ptr framebuffer; +bool antialias = true; +void PaintGraphics() { + const Camera &camera = SS.GW.GetCamera(); + + std::shared_ptr pixmap = std::make_shared(); + pixmap->format = Pixmap::Format::BGRA; + pixmap->width = camera.width; + pixmap->height = camera.height; + pixmap->stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, camera.width); + pixmap->data = std::vector(pixmap->stride * pixmap->height); + cairo_surface_t *surface = + cairo_image_surface_create_for_data(&pixmap->data[0], CAIRO_FORMAT_RGB24, + pixmap->width, pixmap->height, pixmap->stride); + cairo_t *context = cairo_create(surface); + + CairoRenderer canvas; + canvas.camera = camera; + canvas.lighting = SS.GW.GetLighting(); + canvas.chordTolerance = SS.chordTol; + canvas.context = context; + canvas.antialias = antialias; + + SS.GW.Draw(&canvas); + canvas.CullOccludedStrokes(); + canvas.OutputInPaintOrder(); + + pixmap->ConvertTo(Pixmap::Format::RGBA); + framebuffer = pixmap; + + canvas.Clear(); + + cairo_surface_destroy(surface); + cairo_destroy(context); +} + +void SetCurrentFilename(const std::string &filename) { +} +void ToggleFullScreen() { +} +bool FullScreenIsActive() { + return false; +} +void ShowGraphicsEditControl(int x, int y, int fontHeight, int minWidthChars, + const std::string &val) { + ssassert(false, "Not implemented"); +} +void HideGraphicsEditControl() { +} +bool GraphicsEditControlIsVisible() { + return false; +} +void ToggleMenuBar() { +} +bool MenuBarIsVisible() { + return false; +} +void AddContextMenuItem(const char *label, ContextCommand cmd) { + ssassert(false, "Not implemented"); +} +void CreateContextSubmenu() { + ssassert(false, "Not implemented"); +} +ContextCommand ShowContextMenu() { + ssassert(false, "Not implemented"); +} +void EnableMenuByCmd(Command cmd, bool enabled) { +} +void CheckMenuByCmd(Command cmd, bool checked) { +} +void RadioMenuByCmd(Command cmd, bool selected) { +} +void RefreshRecentMenus() { +} + +//----------------------------------------------------------------------------- +// Text window +//----------------------------------------------------------------------------- + +void ShowTextWindow(bool visible) { +} +void GetTextWindowSize(int *w, int *h) { + *w = *h = 100; +} +void InvalidateText() { +} +void MoveTextScrollbarTo(int pos, int maxPos, int page) { +} +void SetMousePointerToHand(bool is_hand) { +} +void ShowTextEditControl(int x, int y, const std::string &val) { + ssassert(false, "Not implemented"); +} +void HideTextEditControl() { +} +bool TextEditControlIsVisible() { + return false; +} + +//----------------------------------------------------------------------------- +// Dialogs +//----------------------------------------------------------------------------- + +bool GetOpenFile(std::string *filename, const std::string &activeOrEmpty, + const FileFilter filters[]) { + ssassert(false, "Not implemented"); +} +bool GetSaveFile(std::string *filename, const std::string &activeOrEmpty, + const FileFilter filters[]) { + ssassert(false, "Not implemented"); +} +DialogChoice SaveFileYesNoCancel() { + ssassert(false, "Not implemented"); +} +DialogChoice LoadAutosaveYesNo() { + ssassert(false, "Not implemented"); +} +DialogChoice LocateImportedFileYesNoCancel(const std::string &filename, + bool canCancel) { + ssassert(false, "Not implemented"); +} +void DoMessageBox(const char *message, int rows, int cols, bool error) { + dbp("%s box: %s", error ? "error" : "message", message); + ssassert(false, "Not implemented"); +} +void OpenWebsite(const char *url) { + ssassert(false, "Not implemented"); +} + +//----------------------------------------------------------------------------- +// Resources +//----------------------------------------------------------------------------- + +std::vector GetFontFiles() { + return {}; +} + +std::string resourceDir; +const void *LoadResource(const std::string &name, size_t *size) { + static std::map> cache; + + auto it = cache.find(name); + if(it == cache.end()) { + std::string path = resourceDir + "/" + name; + std::vector data; + + FILE *f = ssfopen(PathSepUnixToPlatform(path).c_str(), "rb"); + ssassert(f != NULL, "Cannot open resource"); + fseek(f, 0, SEEK_END); + data.resize(ftell(f)); + fseek(f, 0, SEEK_SET); + fread(&data[0], 1, data.size(), f); + fclose(f); + + cache.emplace(name, std::move(data)); + it = cache.find(name); + } + + *size = (*it).second.size(); + return &(*it).second[0]; +} + +//----------------------------------------------------------------------------- +// Application lifecycle +//----------------------------------------------------------------------------- + +void ExitNow() { + ssassert(false, "Not implemented"); +} + +} diff --git a/src/platform/unixutil.cpp b/src/platform/unixutil.cpp index 41703f79..f076cc99 100644 --- a/src/platform/unixutil.cpp +++ b/src/platform/unixutil.cpp @@ -65,6 +65,16 @@ bool PathEqual(const std::string &a, const std::string &b) #endif } +std::string PathSepPlatformToUnix(const std::string &filename) +{ + return filename; +} + +std::string PathSepUnixToPlatform(const std::string &filename) +{ + return filename; +} + FILE *ssfopen(const std::string &filename, const char *mode) { ssassert(filename.length() == strlen(filename.c_str()), diff --git a/src/platform/w32util.cpp b/src/platform/w32util.cpp index 3bdd7ee7..7848c504 100644 --- a/src/platform/w32util.cpp +++ b/src/platform/w32util.cpp @@ -97,6 +97,26 @@ bool PathEqual(const std::string &a, const std::string &b) } +std::string PathSepPlatformToUnix(const std::string &filename) +{ + std::string result = filename; + for(size_t i = 0; i < result.length(); i++) { + if(result[i] == '\\') + result[i] = '/'; + } + return result; +} + +std::string PathSepUnixToPlatform(const std::string &filename) +{ + std::string result = filename; + for(size_t i = 0; i < result.length(); i++) { + if(result[i] == '/') + result[i] = '\\'; + } + return result; +} + FILE *ssfopen(const std::string &filename, const char *mode) { // Prepend \\?\ UNC prefix unless already an UNC path. diff --git a/src/render/render.h b/src/render/render.h index 6839832e..f74b522b 100644 --- a/src/render/render.h +++ b/src/render/render.h @@ -277,6 +277,8 @@ public: class CairoRenderer : public SurfaceRenderer { public: cairo_t *context; + // Renderer configuration. + bool antialias; // Renderer state. struct { hStroke hcs; diff --git a/src/render/rendercairo.cpp b/src/render/rendercairo.cpp index 39ce8e7d..3f7cb995 100644 --- a/src/render/rendercairo.cpp +++ b/src/render/rendercairo.cpp @@ -44,7 +44,11 @@ void CairoRenderer::SelectStroke(hStroke hcs) { cairo_set_dash(context, dashes.data(), dashes.size(), 0); cairo_set_source_rgba(context, color.redF(), color.greenF(), color.blueF(), color.alphaF()); - cairo_set_antialias(context, CAIRO_ANTIALIAS_BEST); + if(antialias) { + cairo_set_antialias(context, CAIRO_ANTIALIAS_GRAY); + } else { + cairo_set_antialias(context, CAIRO_ANTIALIAS_NONE); + } } void CairoRenderer::MoveTo(Vector p) { diff --git a/src/resource.cpp b/src/resource.cpp index c7551d9a..3393e16c 100644 --- a/src/resource.cpp +++ b/src/resource.cpp @@ -95,6 +95,42 @@ RgbaColor Pixmap::GetPixel(size_t x, size_t y) const { ssassert(false, "Unexpected resource format"); } +void Pixmap::SetPixel(size_t x, size_t y, RgbaColor color) { + uint8_t *pixel = &data[y * stride + x * GetBytesPerPixel()]; + + switch(format) { + case Format::RGBA: + pixel[0] = color.red; + pixel[1] = color.green; + pixel[2] = color.blue; + pixel[3] = color.alpha; + break; + + case Format::RGB: + pixel[0] = color.red; + pixel[1] = color.green; + pixel[2] = color.blue; + break; + + case Format::BGRA: + pixel[0] = color.blue; + pixel[1] = color.green; + pixel[2] = color.red; + pixel[3] = color.alpha; + break; + + case Format::BGR: + pixel[0] = color.blue; + pixel[1] = color.green; + pixel[2] = color.red; + break; + + case Format::A: + pixel[0] = color.alpha; + break; + } +} + void Pixmap::ConvertTo(Format newFormat) { switch(format) { case Format::RGBA: @@ -215,6 +251,14 @@ exit: return nullptr; } +std::shared_ptr Pixmap::ReadPng(const std::string &filename, bool flip) { + FILE *f = ssfopen(filename.c_str(), "rb"); + if(!f) return NULL; + std::shared_ptr pixmap = ReadPng(f, flip); + fclose(f); + return pixmap; +} + bool Pixmap::WritePng(FILE *f, bool flip) { int colorType; bool bgr; @@ -262,6 +306,29 @@ exit: return false; } +bool Pixmap::WritePng(const std::string &filename, bool flip) { + FILE *f = ssfopen(filename.c_str(), "wb"); + if(!f) return false; + bool success = WritePng(f, flip); + fclose(f); + return success; +} + +bool Pixmap::Equals(const Pixmap &other) const { + if(format != other.format || width != other.width || height != other.height) { + return false; + } + + size_t rowLength = width * GetBytesPerPixel(); + for(size_t y = 0; y < height; y++) { + if(memcmp(&data[y * stride], &other.data[y * other.stride], rowLength)) { + return false; + } + } + + return true; +} + std::shared_ptr Pixmap::Create(Format format, size_t width, size_t height) { std::shared_ptr pixmap = std::make_shared(); pixmap->format = format; diff --git a/src/resource.h b/src/resource.h index 787e2a1f..41fef780 100644 --- a/src/resource.h +++ b/src/resource.h @@ -36,12 +36,16 @@ public: static std::shared_ptr FromPng(const uint8_t *data, size_t size, bool flip = false); static std::shared_ptr ReadPng(FILE *f, bool flip = false); + static std::shared_ptr ReadPng(const std::string &filename, bool flip = false); bool WritePng(FILE *f, bool flip = false); + bool WritePng(const std::string &filename, bool flip = false); size_t GetBytesPerPixel() const; RgbaColor GetPixel(size_t x, size_t y) const; + bool Equals(const Pixmap &other) const; void ConvertTo(Format newFormat); + void SetPixel(size_t x, size_t y, RgbaColor color); }; class BitmapFont { diff --git a/src/solvespace.cpp b/src/solvespace.cpp index 6245feb0..5361dea5 100644 --- a/src/solvespace.cpp +++ b/src/solvespace.cpp @@ -13,8 +13,10 @@ Sketch SolveSpace::SK = {}; std::string SolveSpace::RecentFile[MAX_RECENT] = {}; void SolveSpaceUI::Init() { +#if !defined(HEADLESS) // Check that the resource system works. dbp("%s", LoadString("banner.txt").data()); +#endif SS.tangentArcRadius = 10.0; diff --git a/src/solvespace.h b/src/solvespace.h index 2dd4fc30..e5bf07d6 100644 --- a/src/solvespace.h +++ b/src/solvespace.h @@ -150,6 +150,8 @@ enum class ContextCommand : uint32_t; extern const bool FLIP_FRAMEBUFFER; bool PathEqual(const std::string &a, const std::string &b); +std::string PathSepPlatformToUnix(const std::string &filename); +std::string PathSepUnixToPlatform(const std::string &filename); FILE *ssfopen(const std::string &filename, const char *mode); void ssremove(const std::string &filename); diff --git a/src/textwin.cpp b/src/textwin.cpp index fb89cdef..2519cb92 100644 --- a/src/textwin.cpp +++ b/src/textwin.cpp @@ -718,6 +718,7 @@ bool TextWindow::DrawOrHitTestColorPicker(UiCanvas *uiCanvas, DrawOrHitHow how, } void TextWindow::Paint() { +#if !defined(HEADLESS) int width, height; GetTextWindowSize(&width, &height); @@ -857,6 +858,7 @@ void TextWindow::Paint() { DrawOrHitTestColorPicker(&uiCanvas, PAINT, false, 0, 0); canvas.EndFrame(); +#endif } void TextWindow::MouseEvent(bool leftClick, bool leftDown, double x, double y) { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 00000000..8b8492dd --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,18 @@ +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR}) + +set(testsuite_SOURCES + harness.cpp + request/line_segment/test.cpp) + +add_executable(solvespace_testsuite + ${testsuite_SOURCES}) + +target_link_libraries(solvespace_testsuite + solvespace_headless) + +add_custom_target(solvespace-test ALL + COMMAND $ + COMMENT "Testing SolveSpace" + VERBATIM) diff --git a/test/harness.cpp b/test/harness.cpp new file mode 100644 index 00000000..4547cd8a --- /dev/null +++ b/test/harness.cpp @@ -0,0 +1,320 @@ +//----------------------------------------------------------------------------- +// Our harness for running test cases, and reusable checks. +// +// Copyright 2016 whitequark +//----------------------------------------------------------------------------- +#include "harness.h" +#include +#if defined(WIN32) +#include +#else +#include +#endif + +namespace SolveSpace { + // These are defined in headless.cpp, and aren't exposed in solvespace.h. + extern std::string resourceDir; + extern bool antialias; + extern std::shared_ptr framebuffer; +} + +// The paths in __FILE__ are from the build system, but defined(WIN32) returns +// the value for the host system. +#define BUILD_PATH_SEP (__FILE__[0]=='/' ? '/' : '\\') +#define HOST_PATH_SEP PATH_SEP + +static std::string BuildRoot() { + static std::string rootDir; + if(!rootDir.empty()) return rootDir; + + rootDir = __FILE__; + rootDir.erase(rootDir.rfind(BUILD_PATH_SEP) + 1); + return rootDir; +} + +static std::string HostRoot() { + static std::string rootDir; + if(!rootDir.empty()) return rootDir; + + // No especially good way to do this, so let's assume somewhere up from + // the current directory there's our repository, with CMakeLists.txt, and + // pivot from there. +#if defined(WIN32) + wchar_t currentDirW[MAX_PATH]; + GetCurrentDirectoryW(MAX_PATH, currentDirW); + rootDir = Narrow(currentDirW); +#else + rootDir = "."; +#endif + + // We're never more than four levels deep. + for(size_t i = 0; i < 4; i++) { + std::string listsPath = rootDir; + listsPath += HOST_PATH_SEP; + listsPath += "CMakeLists.txt"; + FILE *f = ssfopen(listsPath, "r"); + if(f) { + fclose(f); + rootDir += HOST_PATH_SEP; + rootDir += "test"; + return rootDir; + } + + if(rootDir[0] == '.') { + rootDir += HOST_PATH_SEP; + rootDir += ".."; + } else { + rootDir.erase(rootDir.rfind(HOST_PATH_SEP)); + } + } + + ssassert(false, "Couldn't locate repository root"); +} + +enum class Color { + Red, + Green, + DarkGreen +}; + +static std::string Colorize(Color color, std::string input) { +#if !defined(WIN32) + if(isatty(fileno(stdout))) { + switch(color) { + case Color::Red: + return "\e[1;31m" + input + "\e[0m"; + case Color::Green: + return "\e[1;32m" + input + "\e[0m"; + case Color::DarkGreen: + return "\e[36m" + input + "\e[0m"; + } + } +#endif + return input; +} + +static std::vector ReadFile(std::string path) { + std::vector data; + FILE *f = ssfopen(path.c_str(), "rb"); + if(f) { + fseek(f, 0, SEEK_END); + data.resize(ftell(f)); + fseek(f, 0, SEEK_SET); + fread(&data[0], 1, data.size(), f); + fclose(f); + } + return data; +} + +bool Test::Helper::RecordCheck(bool success) { + checkCount++; + if(!success) failCount++; + return success; +} + +void Test::Helper::PrintFailure(const char *file, int line, std::string msg) { + std::string shortFile = file; + shortFile.erase(0, BuildRoot().size()); + fprintf(stderr, "test%c%s:%d: FAILED: %s\n", + BUILD_PATH_SEP, shortFile.c_str(), line, msg.c_str()); +} + +std::string Test::Helper::GetAssetPath(std::string testFile, std::string assetName, + std::string mangle) { + if(!mangle.empty()) { + assetName.insert(assetName.rfind('.'), "." + mangle); + } + testFile.erase(0, BuildRoot().size()); + testFile.erase(testFile.rfind(BUILD_PATH_SEP) + 1); + return PathSepUnixToPlatform(HostRoot() + "/" + testFile + assetName); +} + +bool Test::Helper::CheckTrue(const char *file, int line, const char *expr, bool result) { + if(!RecordCheck(result)) { + PrintFailure(file, line, + ssprintf("(%s) == %s", expr, result ? "true" : "false")); + return false; + } else { + return true; + } +} + +bool Test::Helper::CheckLoad(const char *file, int line, const char *fixture) { + std::string fixturePath = GetAssetPath(file, fixture); + + FILE *f = ssfopen(fixturePath.c_str(), "rb"); + bool fixtureExists = (f != NULL); + if(f) fclose(f); + + bool result = fixtureExists && + SS.LoadFromFile(fixturePath) && SS.ReloadAllImported(/*canCancel=*/false); + if(!RecordCheck(result)) { + PrintFailure(file, line, + ssprintf("loading file '%s'", fixturePath.c_str())); + return false; + } else { + SS.AfterNewFile(); + SS.GW.offset = {}; + SS.GW.scale = 10.0; + return true; + } +} + +bool Test::Helper::CheckSave(const char *file, int line, const char *reference) { + std::string refPath = GetAssetPath(file, reference), + outPath = GetAssetPath(file, reference, "out"); + if(!RecordCheck(SS.SaveToFile(outPath))) { + PrintFailure(file, line, + ssprintf("saving file '%s'", refPath.c_str())); + return false; + } else { + std::vector refData = ReadFile(refPath), + outData = ReadFile(outPath); + if(!RecordCheck(refData == outData)) { + PrintFailure(file, line, "savefile doesn't match reference"); + return false; + } + + ssremove(outPath); + return true; + } +} + +bool Test::Helper::CheckRender(const char *file, int line, const char *reference) { + PaintGraphics(); + + std::string refPath = GetAssetPath(file, reference), + outPath = GetAssetPath(file, reference, "out"), + diffPath = GetAssetPath(file, reference, "diff"); + + std::shared_ptr refPixmap = Pixmap::ReadPng(refPath.c_str(), /*flip=*/true); + if(!RecordCheck(refPixmap && refPixmap->Equals(*framebuffer))) { + framebuffer->WritePng(outPath.c_str(), /*flip=*/true); + + if(!refPixmap) { + PrintFailure(file, line, "reference render not present"); + return false; + } + + ssassert(refPixmap->format == framebuffer->format, "Expected buffer formats to match"); + if(refPixmap->width != framebuffer->width || + refPixmap->height != framebuffer->height) { + PrintFailure(file, line, "render doesn't match reference; dimensions differ"); + } else { + std::shared_ptr diffPixmap = + Pixmap::Create(refPixmap->format, refPixmap->width, refPixmap->height); + + int diffPixelCount = 0; + for(size_t j = 0; j < refPixmap->height; j++) { + for(size_t i = 0; i < refPixmap->width; i++) { + if(!refPixmap->GetPixel(i, j).Equals(framebuffer->GetPixel(i, j))) { + diffPixelCount++; + diffPixmap->SetPixel(i, j, RgbaColor::From(255, 0, 0, 255)); + } + } + } + + diffPixmap->WritePng(diffPath.c_str(), /*flip=*/true); + std::string message = + ssprintf("render doesn't match reference; %d (%.2f%%) pixels differ", + diffPixelCount, + 100.0 * diffPixelCount / (refPixmap->width * refPixmap->height)); + PrintFailure(file, line, message); + } + return false; + } else { + ssremove(outPath); + ssremove(diffPath); + return true; + } +} + +// Avoid global constructors; using a global static vector instead of a local one +// breaks MinGW for some obscure reason. +static std::vector *testCasesPtr; +int Test::Case::Register(Test::Case testCase) { + static std::vector testCases; + testCases.push_back(testCase); + testCasesPtr = &testCases; + return 0; +} + +int main(int argc, char **argv) { +#if defined(WIN32) + _set_abort_behavior(0, _WRITE_ABORT_MSG); + InitHeaps(); +#endif + + std::regex filter(".*"); + if(argc == 1) { + } else if(argc == 2) { + filter = argv[1]; + } else { + fprintf(stderr, "Usage: %s [test filter regex]\n", argv[0]); + return 1; + } + + resourceDir = HostRoot(); + resourceDir.erase(resourceDir.rfind(HOST_PATH_SEP) + 1); + resourceDir += "res"; + + // Different Cairo versions have different antialiasing algorithms. + antialias = false; + + // Wreck order dependencies between tests! + std::random_shuffle(testCasesPtr->begin(), testCasesPtr->end()); + + auto testStartTime = std::chrono::steady_clock::now(); + size_t ranTally = 0, skippedTally = 0, checkTally = 0, failTally = 0; + for(Test::Case &testCase : *testCasesPtr) { + std::string testCaseName = testCase.fileName; + testCaseName.erase(0, BuildRoot().size()); + testCaseName.erase(testCaseName.rfind(BUILD_PATH_SEP)); + testCaseName += BUILD_PATH_SEP + testCase.caseName; + + std::smatch filterMatch; + if(!std::regex_search(testCaseName, filterMatch, filter)) { + skippedTally += 1; + continue; + } + + SS.Init(); + + Test::Helper helper = {}; + testCase.fn(&helper); + + SK.Clear(); + SS.Clear(); + + ranTally += 1; + checkTally += helper.checkCount; + failTally += helper.failCount; + if(helper.checkCount == 0) { + fprintf(stderr, " %s test %s (empty)\n", + Colorize(Color::Red, "??").c_str(), + Colorize(Color::DarkGreen, testCaseName).c_str()); + } else if(helper.failCount > 0) { + fprintf(stderr, " %s test %s\n", + Colorize(Color::Red, "NG").c_str(), + Colorize(Color::DarkGreen, testCaseName).c_str()); + } else { + fprintf(stderr, " %s test %s\n", + Colorize(Color::Green, "OK").c_str(), + Colorize(Color::DarkGreen, testCaseName).c_str()); + } + } + + auto testEndTime = std::chrono::steady_clock::now(); + std::chrono::duration testTime = testEndTime - testStartTime; + + if(failTally > 0) { + fprintf(stderr, "Failure! %u checks failed\n", + (unsigned)failTally); + return 1; + } else { + fprintf(stderr, "Success! %u test cases (%u skipped), %u checks, %.3fs\n", + (unsigned)ranTally, (unsigned)skippedTally, + (unsigned)checkTally, testTime.count()); + return 0; + } +} diff --git a/test/harness.h b/test/harness.h new file mode 100644 index 00000000..f3e79282 --- /dev/null +++ b/test/harness.h @@ -0,0 +1,57 @@ +//----------------------------------------------------------------------------- +// Our harness for running test cases, and reusable checks. +// +// Copyright 2016 whitequark +//----------------------------------------------------------------------------- +#include "solvespace.h" + +// Hack... we should rename the one in ui.h instead. +#undef CHECK_TRUE + +namespace SolveSpace { +namespace Test { + +class Helper { +public: + size_t checkCount; + size_t failCount; + + bool RecordCheck(bool success); + void PrintFailure(const char *file, int line, std::string msg); + std::string GetAssetPath(std::string testFile, std::string assetName, + std::string mangle = ""); + + bool CheckTrue(const char *file, int line, const char *expr, bool result); + bool CheckLoad(const char *file, int line, const char *fixture); + bool CheckSave(const char *file, int line, const char *reference); + bool CheckRender(const char *file, int line, const char *fixture); +}; + +class Case { +public: + std::string fileName; + std::string caseName; + std::function fn; + + static int Register(Case testCase); +}; + +} +} + +using namespace SolveSpace; + +#define TEST_CASE(name) \ + static void Test_##name(Test::Helper *); \ + static Test::Case TestCase_##name = { __FILE__, #name, Test_##name }; \ + static int TestReg_##name = Test::Case::Register(TestCase_##name); \ + static void Test_##name(Test::Helper *helper) // { ... } + +#define CHECK_TRUE(cond) \ + do { if(!helper->CheckTrue(__FILE__, __LINE__, #cond, cond)) return; } while(0) +#define CHECK_LOAD(fixture) \ + do { if(!helper->CheckLoad(__FILE__, __LINE__, fixture)) return; } while(0) +#define CHECK_SAVE(fixture) \ + do { if(!helper->CheckSave(__FILE__, __LINE__, fixture)) return; } while(0) +#define CHECK_RENDER(reference) \ + do { if(!helper->CheckRender(__FILE__, __LINE__, reference)) return; } while(0) diff --git a/test/request/line_segment/line_segment.png b/test/request/line_segment/line_segment.png new file mode 100644 index 0000000000000000000000000000000000000000..d1dfb503e50e16d407b8958dd84037ba7b039132 GIT binary patch literal 4300 zcmcIo3sh2R9{)h`RhDL{m1R>i){L5z=3`Q^m*r8LrhKJ|G^CYU_VAIK3QgrS8Sab? zJ}TLcX)~lq4>tApIuaS{m>#6(Jez0O+N;ZQtT`=K27~^VBOJ!;7zh>1Vl9D8A zN+=VV+?SQoSMnF8rYJaooXus27oW6i)xFjbEo4P_sRmqr&{kC-&ZO=I14;0l?f`$DMNT5X?KP8iBEz?7h4ma8m4Xi}_XR0=y=9*% zb+z29r6^t>0P77^s(PG)-(Thn=$EF4kU$J>UI;vXSCiH+%jc-#bG^T>Xmm@2^Tae zY5?a%R`1UbYb4+5T#vJliRquEV>XFd(I{cHEd)x03qMWikqzq#?|*C3jxkxnh#^h zx?fE}##K4LdRii@RS!a0U$}m)f+Zm5XxZ`UvZh+U-W!IEI)FUn#m$x`t{)k`HGH+4 zWqv`pr9)~UZKI4|(4#Ct#vCOMXToPQkT@I9R^92cCIQ}-OQmrx1CVh^Ufe8ooRfIm zkFeZ{@*i(z5DLe*se&bEYlWP7V5KXrDkV`OxrULB^jt}Diu}UMTRQfZ!h@tzik+#X z`}t+W$|}pPuu2rw9^EG@|DOelZbE;gPIs= zh3vwsR4hgJ;?K(uKDyZkK-uuIw^qbh0sHwxl%6Ojd$s$KDx%F4Rkt)^zWVl1)x!in z7C_-xS2E=rT^{}%if+(kVQ)o5nY2b&6(?CFN#pb|eTBRb1P>PTm5@Q#>jR`gll*4u z@z$tLd>iap6YnLJoK}DDI{%`M3SnbV<*6E>uwc-o!nY;Wk{8bBj`ok1BuOyh_c771 z1K0hO9xSO>(5Td)l@(c5;#)CC{%Bra4U3S6AYR+t9%fexerZJCu~1Y) zmVg}l^JX+@D5J6H%;nN^Vi@5hez!QE?AsCJ@>mC5h16Q)>*CFe(MNhP4PFutPl`bO z6mFEztnBWvs*~>~<{1OXN7h*5;SukUQ`P0P!-` zLiQdN#Q04!O-r?^sm9I6tVJ>DBoK=%jwNe=wlo>XnFmF>?RRGSw`zeW#1@3r9YiUm zi%+|(O3Y3Y5xpQSq6<4yjg;r7G?)6Yz<>qjaw(izIETQYko;ivv~+)U69I+~eNcYF z$&9mVBcU^eS+(usu#-xn1h_fKr(yyfLx$~VFVF?_wK5Y-dGgotQs0>#rRdEHSeopI z*uRzhL`QCrb9IuBsTj`0tP^6)v}@JI3Nl*DicNgvSz&77B=J9CstVy2ju-yq;%#c6 zzg8BFxad(0LCehKa>I&FgG~g4#8kkt*)}@}{kvvF6{mMRVm0N!a93?<|MN`vx)zcH0+I?7oKGBh&p>`} zY)yCOcGMbE=BaQj3G6leZ3$=H)}chiGx46i6PhLX%6Rr0uOQ3Em{ zijo8_ZWNrRV-pAa&tU5r)slZ+eqi+{KwsFP#$Q(w=KN!sz%)<|Jg{e4K5jh2w`J!< zm{-Kqw<5#q7W0cb#VxH@;~u6y&kGKC#>MVycsi>2_nq9LF|R`R*Msv*9ZHIM4V|cK z4YoD5J(iExh}gRTM2U;gPh9+Y&{F=fOE;W`DPIeGziW6ukklhxOIvPDF_pmKVWK_p z$@(Fi-_LQgy1Tyy_EQ9K7q*Le^pW)*-eI@QUJFdySLM} H;?DgGHx|g{ literal 0 HcmV?d00001 diff --git a/test/request/line_segment/line_segment_v20.slvs b/test/request/line_segment/line_segment_v20.slvs new file mode 100644 index 00000000..fb26b9ff --- /dev/null +++ b/test/request/line_segment/line_segment_v20.slvs @@ -0,0 +1,278 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + diff --git a/test/request/line_segment/line_segment_v21.slvs b/test/request/line_segment/line_segment_v21.slvs new file mode 100644 index 00000000..167a2c27 --- /dev/null +++ b/test/request/line_segment/line_segment_v21.slvs @@ -0,0 +1,278 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + diff --git a/test/request/line_segment/test.cpp b/test/request/line_segment/test.cpp new file mode 100644 index 00000000..b203ace4 --- /dev/null +++ b/test/request/line_segment/test.cpp @@ -0,0 +1,17 @@ +#include "harness.h" + +TEST_CASE(load_v20) { + CHECK_LOAD("line_segment_v20.slvs"); + CHECK_RENDER("line_segment.png"); +} + +TEST_CASE(roundtrip_v21) { + CHECK_LOAD("line_segment_v21.slvs"); + CHECK_RENDER("line_segment.png"); + CHECK_SAVE("line_segment_v21.slvs"); +} + +TEST_CASE(migrate_v20_to_v21) { + CHECK_LOAD("line_segment_v20.slvs"); + CHECK_SAVE("line_segment_v21.slvs"); +}