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.
pull/33/head
whitequark 2016-07-25 19:37:48 +00:00
parent 977a0b8e6d
commit 5e63d8301e
28 changed files with 1482 additions and 130 deletions

30
.gitattributes vendored
View File

@ -1,22 +1,10 @@
# .gitattributes for SolveSpace
# Set default behaviour, in case users don't have core.autocrlf set.
* text=auto * text=auto
*.cpp text
# Explicitly declare text files we want to always be normalized and converted *.h text
# to native line endings on checkout. *.txt text
*.cpp text *.gz binary
*.h text *.ico binary
*.txt text *.jpg binary
*.lib binary
# Declare files that will always have CRLF line endings on checkout. *.png binary
*.sln text eol=crlf *.slvs binary
# Denote all files that are truly binary and should not be modified.
*.gz binary
*.ico binary
*.jpg binary
*.lib binary
*.png binary
# end .gitattributes

2
.gitignore vendored
View File

@ -1,5 +1,7 @@
/CMakeCache.txt /CMakeCache.txt
/build*/ /build*/
/test/**/*.diff.*
/test/**/*.curr.*
*.trace # OpenGL apitrace files *.trace # OpenGL apitrace files
/debian/tmp/ /debian/tmp/
/debian/*.log /debian/*.log

View File

@ -2,5 +2,7 @@
if echo $TRAVIS_TAG | grep ^v; then BUILD_TYPE=RelWithDebInfo; else BUILD_TYPE=Debug; fi if echo $TRAVIS_TAG | grep ^v; then BUILD_TYPE=RelWithDebInfo; else BUILD_TYPE=Debug; fi
export BUILD_TYPE mkdir build
dpkg-buildpackage -b -us -uc cd build
cmake -DCMAKE_C_COMPILER=gcc-5 -DCMAKE_CXX_COMPILER=g++-5 -DCMAKE_BUILD_TYPE=$BUILD_TYPE ..
make VERBOSE=1

View File

@ -27,6 +27,8 @@ set(solvespace_VERSION_MAJOR 3)
set(solvespace_VERSION_MINOR 0) set(solvespace_VERSION_MINOR 0)
string(SUBSTRING "${GIT_COMMIT_HASH}" 0 8 solvespace_GIT_HASH) 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) if(NOT WIN32 AND NOT APPLE)
set(GUI gtk2 CACHE STRING "GUI toolkit to use (one of: gtk2 gtk3)") set(GUI gtk2 CACHE STRING "GUI toolkit to use (one of: gtk2 gtk3)")
endif() endif()
@ -93,19 +95,21 @@ if(WIN32)
PNG_PNG_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/extlib/libpng) PNG_PNG_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/extlib/libpng)
list(APPEND PNG_PNG_INCLUDE_DIR ${CMAKE_BINARY_DIR}/extlib/libpng) list(APPEND PNG_PNG_INCLUDE_DIR ${CMAKE_BINARY_DIR}/extlib/libpng)
message(STATUS "Using in-tree pixman") if(ENABLE_TESTS)
add_vendored_subdirectory(extlib/pixman) message(STATUS "Using in-tree pixman")
set(PIXMAN_FOUND YES) add_vendored_subdirectory(extlib/pixman)
set(PIXMAN_LIBRARY pixman) set(PIXMAN_FOUND YES)
set(PIXMAN_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/pixman/pixman) set(PIXMAN_LIBRARY pixman)
list(APPEND PIXMAN_INCLUDE_DIRS ${CMAKE_BINARY_DIR}/extlib/pixman/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") message(STATUS "Using in-tree cairo")
add_vendored_subdirectory(extlib/cairo) add_vendored_subdirectory(extlib/cairo)
set(CAIRO_FOUND YES) set(CAIRO_FOUND YES)
set(CAIRO_LIBRARY cairo) set(CAIRO_LIBRARIES cairo)
set(CAIRO_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/cairo/src) set(CAIRO_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/cairo/src)
list(APPEND CAIRO_INCLUDE_DIRS ${CMAKE_BINARY_DIR}/extlib/cairo/src) list(APPEND CAIRO_INCLUDE_DIRS ${CMAKE_BINARY_DIR}/extlib/cairo/src)
endif()
if(NOT MINGW) if(NOT MINGW)
message(STATUS "Using prebuilt SpaceWare") message(STATUS "Using prebuilt SpaceWare")
@ -122,10 +126,15 @@ elseif(APPLE)
find_package(PNG REQUIRED) find_package(PNG REQUIRED)
find_package(Freetype 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(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 else() # Linux and compatible systems
find_package(PkgConfig REQUIRED)
find_package(Backtrace) find_package(Backtrace)
find_package(SpaceWare) find_package(SpaceWare)
@ -133,12 +142,13 @@ else() # Linux and compatible systems
find_package(PNG REQUIRED) find_package(PNG REQUIRED)
find_package(Freetype REQUIRED) find_package(Freetype REQUIRED)
# Use freedesktop's pkg-config to locate everything else. if(ENABLE_TESTS)
find_package(PkgConfig REQUIRED) pkg_check_modules(CAIRO REQUIRED cairo)
endif()
pkg_check_modules(FONTCONFIG REQUIRED fontconfig) pkg_check_modules(FONTCONFIG REQUIRED fontconfig)
pkg_check_modules(JSONC REQUIRED json-c) pkg_check_modules(JSONC REQUIRED json-c)
pkg_check_modules(FREETYPE REQUIRED freetype2) pkg_check_modules(FREETYPE REQUIRED freetype2)
pkg_check_modules(CAIRO REQUIRED cairo)
set(HAVE_GTK TRUE) set(HAVE_GTK TRUE)
if(GUI STREQUAL "gtk3") 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") set(WARNING_FLAGS "${WARNING_FLAGS} -Werror=switch")
endif() endif()
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WARNING_FLAGS}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WARNING_FLAGS}")
if(WIN32) if(WIN32)
set(CMAKE_RC_FLAGS "${CMAKE_RC_FLAGS} -l0") set(CMAKE_RC_FLAGS "${CMAKE_RC_FLAGS} -l0")
endif() endif()
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WARNING_FLAGS}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WARNING_FLAGS}")
# components # components
add_subdirectory(res) add_subdirectory(res)
add_subdirectory(src) add_subdirectory(src)
add_subdirectory(exposed) add_subdirectory(exposed)
if(ENABLE_TESTS)
add_subdirectory(test)
endif()

View File

@ -26,12 +26,12 @@ Building on Linux
### Building for Linux ### Building for Linux
You will need CMake, libpng, zlib, json-c, fontconfig, freetype, gtkmm 2.4, 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: On a Debian derivative (e.g. Ubuntu) these can be installed with:
apt-get install libpng-dev libjson-c-dev libfreetype6-dev \ apt-get install libpng-dev libjson-c-dev libfreetype6-dev \
libfontconfig1-dev libgtkmm-2.4-dev libpangomm-1.4-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: Before building, check out the necessary submodules:
@ -41,7 +41,7 @@ After that, build SolveSpace as following:
mkdir build mkdir build
cd build cd build
cmake .. cmake .. -DENABLE_TESTS=OFF
make make
sudo make install sudo make install
@ -63,15 +63,17 @@ After that, build 32-bit SolveSpace as following:
mkdir build mkdir build
cd build cd build
cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/Toolchain-mingw32.cmake .. cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/Toolchain-mingw32.cmake \
make solvespace -DENABLE_TESTS=OFF
make
Or, build 64-bit SolveSpace as following: Or, build 64-bit SolveSpace as following:
mkdir build mkdir build
cd build cd build
cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/Toolchain-mingw64.cmake .. cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/Toolchain-mingw64.cmake \
make solvespace -DENABLE_TESTS=OFF
make
The application is built as `build/src/solvespace.exe`. 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 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: [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. 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 mkdir build
cd build cd build
cmake .. cmake .. -DENABLE_TESTS=OFF
make make
The app bundle is built in `build/src/solvespace.app`. 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 git submodule update --init
mkdir build mkdir build
cd build cd build
cmake .. -G "NMake Makefiles" cmake .. -G "NMake Makefiles" -DENABLE_TESTS=OFF
nmake nmake
### MSVC build ### MSVC build
@ -137,7 +140,7 @@ in bash:
git submodule update --init git submodule update --init
mkdir build mkdir build
cd build cd build
cmake .. cmake .. -DENABLE_TESTS=OFF
make make
[cmakewin]: http://www.cmake.org/download/#latest [cmakewin]: http://www.cmake.org/download/#latest

View File

@ -6,9 +6,12 @@ before_build:
- cd build - cd build
- set tag=x%APPVEYOR_REPO_TAG_NAME% - set tag=x%APPVEYOR_REPO_TAG_NAME%
- if %tag:~,2% == xv (set BUILD_TYPE=RelWithDebInfo) else (set BUILD_TYPE=Debug) - 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: build_script:
- msbuild "src\solvespace.vcxproj" /verbosity:minimal /property:Configuration=%BUILD_TYPE% /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" - 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: artifacts:
- path: build\src\Debug\solvespace.exe - path: build\src\Debug\solvespace.exe
name: solvespace.exe name: solvespace.exe

View File

@ -72,7 +72,8 @@ endif()
if(WIN32) if(WIN32)
set(platform_SOURCES set(platform_SOURCES
platform/w32main.cpp) platform/w32main.cpp
render/rendergl1.cpp)
set(platform_LIBRARIES set(platform_LIBRARIES
comctl32 comctl32
@ -83,7 +84,8 @@ elseif(APPLE)
set(platform_SOURCES set(platform_SOURCES
platform/cocoamain.mm platform/cocoamain.mm
render/rendergl.cpp) render/rendergl.cpp
render/rendergl1.cpp)
set(platform_BUNDLED_LIBS set(platform_BUNDLED_LIBS
${PNG_LIBRARIES} ${PNG_LIBRARIES}
@ -94,10 +96,10 @@ elseif(APPLE)
elseif(HAVE_GTK) elseif(HAVE_GTK)
set(platform_SOURCES set(platform_SOURCES
platform/gtkmain.cpp platform/gtkmain.cpp
render/rendergl.cpp) render/rendergl.cpp
render/rendergl1.cpp)
set(platform_LIBRARIES set(platform_LIBRARIES
${Backtrace_LIBRARIES}
${SPACEWARE_LIBRARIES}) ${SPACEWARE_LIBRARIES})
foreach(pkg_config_lib GTKMM JSONC FONTCONFIG) foreach(pkg_config_lib GTKMM JSONC FONTCONFIG)
@ -107,9 +109,9 @@ elseif(HAVE_GTK)
endforeach() endforeach()
endif() endif()
# solvespace executable # solvespace library
set(solvespace_HEADERS set(solvespace_cad_HEADERS
config.h config.h
dsc.h dsc.h
expr.h expr.h
@ -120,18 +122,15 @@ set(solvespace_HEADERS
render/render.h render/render.h
srf/surface.h) srf/surface.h)
set(solvespace_SOURCES set(solvespace_cad_SOURCES
bsp.cpp bsp.cpp
clipboard.cpp clipboard.cpp
confscreen.cpp
constraint.cpp constraint.cpp
constrainteq.cpp constrainteq.cpp
describescreen.cpp describescreen.cpp
draw.cpp
drawconstraint.cpp drawconstraint.cpp
drawentity.cpp drawentity.cpp
entity.cpp entity.cpp
export.cpp
exportstep.cpp exportstep.cpp
exportvector.cpp exportvector.cpp
expr.cpp expr.cpp
@ -147,20 +146,16 @@ set(solvespace_SOURCES
polygon.cpp polygon.cpp
resource.cpp resource.cpp
request.cpp request.cpp
solvespace.cpp
style.cpp style.cpp
system.cpp system.cpp
textscreens.cpp textscreens.cpp
textwin.cpp
toolbar.cpp toolbar.cpp
ttf.cpp ttf.cpp
undoredo.cpp undoredo.cpp
util.cpp util.cpp
view.cpp view.cpp
render/render.cpp render/render.cpp
render/rendergl1.cpp
render/render2d.cpp render/render2d.cpp
render/rendercairo.cpp
srf/boolean.cpp srf/boolean.cpp
srf/curve.cpp srf/curve.cpp
srf/merge.cpp srf/merge.cpp
@ -170,25 +165,38 @@ set(solvespace_SOURCES
srf/surfinter.cpp srf/surfinter.cpp
srf/triangulate.cpp) srf/triangulate.cpp)
add_executable(solvespace WIN32 MACOSX_BUNDLE set(solvespace_cad_gl_SOURCES
${libslvs_HEADERS} confscreen.cpp
${libslvs_SOURCES} draw.cpp
export.cpp
solvespace.cpp
textwin.cpp)
add_library(solvespace_cad STATIC
${util_SOURCES} ${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} ${platform_SOURCES}
${solvespace_HEADERS}
${solvespace_SOURCES}
$<TARGET_PROPERTY:resources,EXTRA_SOURCES>) $<TARGET_PROPERTY:resources,EXTRA_SOURCES>)
add_dependencies(solvespace add_dependencies(solvespace
resources) resources)
target_link_libraries(solvespace target_link_libraries(solvespace
dxfrw solvespace_cad
${OPENGL_LIBRARIES} ${OPENGL_LIBRARIES}
${ZLIB_LIBRARY}
${PNG_LIBRARY}
${FREETYPE_LIBRARY}
${CAIRO_LIBRARY}
${platform_LIBRARIES}) ${platform_LIBRARIES})
if(WIN32 AND NOT MINGW) if(WIN32 AND NOT MINGW)
@ -229,21 +237,22 @@ if(NOT WIN32)
BUNDLE DESTINATION .) BUNDLE DESTINATION .)
endif() endif()
# valgrind # solvespace headless library
add_custom_target(solvespace-valgrind set(headless_SOURCES
valgrind platform/headless.cpp
--tool=memcheck render/rendercairo.cpp)
--verbose
--track-fds=yes add_library(solvespace_headless STATIC EXCLUDE_FROM_ALL
--log-file=vg.%p.out ${solvespace_cad_gl_SOURCES}
--num-callers=50 ${headless_SOURCES})
--error-limit=no
--read-var-info=yes target_compile_definitions(solvespace_headless
--leak-check=full PRIVATE -DHEADLESS)
--leak-resolution=high
--show-reachable=yes target_include_directories(solvespace_headless
--track-origins=yes INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
--malloc-fill=0xac
--free-fill=0xde target_link_libraries(solvespace_headless
$<TARGET_FILE:solvespace>) solvespace_cad
${CAIRO_LIBRARIES})

View File

@ -302,12 +302,14 @@ void TextWindow::ShowConfiguration() {
Printf(false, "%Ba %d %Fl%Ll%f[change]%E", Printf(false, "%Ba %d %Fl%Ll%f[change]%E",
SS.autosaveInterval, &ScreenChangeAutosaveInterval); SS.autosaveInterval, &ScreenChangeAutosaveInterval);
#if !defined(HEADLESS)
const char *gl_vendor, *gl_renderer, *gl_version; const char *gl_vendor, *gl_renderer, *gl_version;
OpenGl1Renderer::GetIdent(&gl_vendor, &gl_renderer, &gl_version); OpenGl1Renderer::GetIdent(&gl_vendor, &gl_renderer, &gl_version);
Printf(false, ""); Printf(false, "");
Printf(false, " %Ftgl vendor %E%s", gl_vendor); Printf(false, " %Ftgl vendor %E%s", gl_vendor);
Printf(false, " %Ft renderer %E%s", gl_renderer); Printf(false, " %Ft renderer %E%s", gl_renderer);
Printf(false, " %Ft version %E%s", gl_version); Printf(false, " %Ft version %E%s", gl_version);
#endif
} }
bool TextWindow::EditControlDoneForConfiguration(const char *s) { bool TextWindow::EditControlDoneForConfiguration(const char *s) {

View File

@ -710,6 +710,7 @@ void GraphicsWindow::Draw(Canvas *canvas) {
} }
void GraphicsWindow::Paint() { void GraphicsWindow::Paint() {
#if !defined(HEADLESS)
havePainted = true; havePainted = true;
auto renderStartTime = std::chrono::high_resolution_clock::now(); auto renderStartTime = std::chrono::high_resolution_clock::now();
@ -786,4 +787,5 @@ void GraphicsWindow::Paint() {
} }
canvas.EndFrame(); canvas.EndFrame();
#endif
} }

View File

@ -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. // rendering the view in the usual way and then copying the pixels.
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void SolveSpaceUI::ExportAsPngTo(const std::string &filename) { void SolveSpaceUI::ExportAsPngTo(const std::string &filename) {
#if !defined(HEADLESS)
// No guarantee that the back buffer contains anything valid right now, // No guarantee that the back buffer contains anything valid right now,
// so repaint the scene. And hide the toolbar too. // so repaint the scene. And hide the toolbar too.
bool prevShowToolbar = SS.showToolbar; bool prevShowToolbar = SS.showToolbar;
SS.showToolbar = false; SS.showToolbar = false;
#ifndef WIN32 #if !defined(WIN32)
GlOffscreen offscreen; GlOffscreen offscreen;
offscreen.Render((int)SS.GW.width, (int)SS.GW.height, [&] { offscreen.Render((int)SS.GW.width, (int)SS.GW.height, [&] {
SS.GW.Paint(); SS.GW.Paint();
@ -1109,10 +1110,11 @@ void SolveSpaceUI::ExportAsPngTo(const std::string &filename) {
} }
if(f) fclose(f); if(f) fclose(f);
#ifndef WIN32 #if !defined(WIN32)
offscreen.Clear(); offscreen.Clear();
#endif #endif
return; return;
#endif
} }

View File

@ -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) bool SolveSpaceUI::ReloadAllImported(bool canCancel)
{ {
std::map<std::string, std::string> linkMap; std::map<std::string, std::string> linkMap;
@ -791,7 +763,7 @@ bool SolveSpaceUI::ReloadAllImported(bool canCancel)
// In a newly created group we only have an absolute path. // In a newly created group we only have an absolute path.
if(!g->linkFileRel.empty()) { if(!g->linkFileRel.empty()) {
std::string rel = PathSepUNIXToPlatform(g->linkFileRel); std::string rel = PathSepUnixToPlatform(g->linkFileRel);
std::string fromRel = MakePathAbsolute(SS.saveFile, rel); std::string fromRel = MakePathAbsolute(SS.saveFile, rel);
FILE *test = ssfopen(fromRel, "rb"); FILE *test = ssfopen(fromRel, "rb");
if(test) { if(test) {
@ -812,7 +784,7 @@ try_load_file:
// Record the linked file's name relative to our filename; // Record the linked file's name relative to our filename;
// if the entire tree moves, then everything will still work // if the entire tree moves, then everything will still work
std::string rel = MakePathRelative(SS.saveFile, g->linkFile); std::string rel = MakePathRelative(SS.saveFile, g->linkFile);
g->linkFileRel = PathSepPlatformToUNIX(rel); g->linkFileRel = PathSepPlatformToUnix(rel);
} else { } else {
// We're not yet saved, so can't make it absolute. // We're not yet saved, so can't make it absolute.
// This will only be used for display purposes, as SS.saveFile // This will only be used for display purposes, as SS.saveFile

273
src/platform/headless.cpp Normal file
View File

@ -0,0 +1,273 @@
//-----------------------------------------------------------------------------
// Our main() function for the headless (no OpenGL) test runner.
//
// Copyright 2016 whitequark
//-----------------------------------------------------------------------------
#include "solvespace.h"
#include <cairo.h>
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<std::string, Setting> 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<Pixmap> framebuffer;
bool antialias = true;
void PaintGraphics() {
const Camera &camera = SS.GW.GetCamera();
std::shared_ptr<Pixmap> pixmap = std::make_shared<Pixmap>();
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<uint8_t>(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<std::string> GetFontFiles() {
return {};
}
std::string resourceDir;
const void *LoadResource(const std::string &name, size_t *size) {
static std::map<std::string, std::vector<uint8_t>> cache;
auto it = cache.find(name);
if(it == cache.end()) {
std::string path = resourceDir + "/" + name;
std::vector<uint8_t> 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");
}
}

View File

@ -65,6 +65,16 @@ bool PathEqual(const std::string &a, const std::string &b)
#endif #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) FILE *ssfopen(const std::string &filename, const char *mode)
{ {
ssassert(filename.length() == strlen(filename.c_str()), ssassert(filename.length() == strlen(filename.c_str()),

View File

@ -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) FILE *ssfopen(const std::string &filename, const char *mode)
{ {
// Prepend \\?\ UNC prefix unless already an UNC path. // Prepend \\?\ UNC prefix unless already an UNC path.

View File

@ -277,6 +277,8 @@ public:
class CairoRenderer : public SurfaceRenderer { class CairoRenderer : public SurfaceRenderer {
public: public:
cairo_t *context; cairo_t *context;
// Renderer configuration.
bool antialias;
// Renderer state. // Renderer state.
struct { struct {
hStroke hcs; hStroke hcs;

View File

@ -44,7 +44,11 @@ void CairoRenderer::SelectStroke(hStroke hcs) {
cairo_set_dash(context, dashes.data(), dashes.size(), 0); cairo_set_dash(context, dashes.data(), dashes.size(), 0);
cairo_set_source_rgba(context, color.redF(), color.greenF(), color.blueF(), cairo_set_source_rgba(context, color.redF(), color.greenF(), color.blueF(),
color.alphaF()); 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) { void CairoRenderer::MoveTo(Vector p) {

View File

@ -95,6 +95,42 @@ RgbaColor Pixmap::GetPixel(size_t x, size_t y) const {
ssassert(false, "Unexpected resource format"); 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) { void Pixmap::ConvertTo(Format newFormat) {
switch(format) { switch(format) {
case Format::RGBA: case Format::RGBA:
@ -215,6 +251,14 @@ exit:
return nullptr; return nullptr;
} }
std::shared_ptr<Pixmap> Pixmap::ReadPng(const std::string &filename, bool flip) {
FILE *f = ssfopen(filename.c_str(), "rb");
if(!f) return NULL;
std::shared_ptr<Pixmap> pixmap = ReadPng(f, flip);
fclose(f);
return pixmap;
}
bool Pixmap::WritePng(FILE *f, bool flip) { bool Pixmap::WritePng(FILE *f, bool flip) {
int colorType; int colorType;
bool bgr; bool bgr;
@ -262,6 +306,29 @@ exit:
return false; 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> Pixmap::Create(Format format, size_t width, size_t height) { std::shared_ptr<Pixmap> Pixmap::Create(Format format, size_t width, size_t height) {
std::shared_ptr<Pixmap> pixmap = std::make_shared<Pixmap>(); std::shared_ptr<Pixmap> pixmap = std::make_shared<Pixmap>();
pixmap->format = format; pixmap->format = format;

View File

@ -36,12 +36,16 @@ public:
static std::shared_ptr<Pixmap> FromPng(const uint8_t *data, size_t size, bool flip = false); static std::shared_ptr<Pixmap> FromPng(const uint8_t *data, size_t size, bool flip = false);
static std::shared_ptr<Pixmap> ReadPng(FILE *f, bool flip = false); static std::shared_ptr<Pixmap> ReadPng(FILE *f, bool flip = false);
static std::shared_ptr<Pixmap> ReadPng(const std::string &filename, bool flip = false);
bool WritePng(FILE *f, bool flip = false); bool WritePng(FILE *f, bool flip = false);
bool WritePng(const std::string &filename, bool flip = false);
size_t GetBytesPerPixel() const; size_t GetBytesPerPixel() const;
RgbaColor GetPixel(size_t x, size_t y) const; RgbaColor GetPixel(size_t x, size_t y) const;
bool Equals(const Pixmap &other) const;
void ConvertTo(Format newFormat); void ConvertTo(Format newFormat);
void SetPixel(size_t x, size_t y, RgbaColor color);
}; };
class BitmapFont { class BitmapFont {

View File

@ -13,8 +13,10 @@ Sketch SolveSpace::SK = {};
std::string SolveSpace::RecentFile[MAX_RECENT] = {}; std::string SolveSpace::RecentFile[MAX_RECENT] = {};
void SolveSpaceUI::Init() { void SolveSpaceUI::Init() {
#if !defined(HEADLESS)
// Check that the resource system works. // Check that the resource system works.
dbp("%s", LoadString("banner.txt").data()); dbp("%s", LoadString("banner.txt").data());
#endif
SS.tangentArcRadius = 10.0; SS.tangentArcRadius = 10.0;

View File

@ -150,6 +150,8 @@ enum class ContextCommand : uint32_t;
extern const bool FLIP_FRAMEBUFFER; extern const bool FLIP_FRAMEBUFFER;
bool PathEqual(const std::string &a, const std::string &b); 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); FILE *ssfopen(const std::string &filename, const char *mode);
void ssremove(const std::string &filename); void ssremove(const std::string &filename);

View File

@ -718,6 +718,7 @@ bool TextWindow::DrawOrHitTestColorPicker(UiCanvas *uiCanvas, DrawOrHitHow how,
} }
void TextWindow::Paint() { void TextWindow::Paint() {
#if !defined(HEADLESS)
int width, height; int width, height;
GetTextWindowSize(&width, &height); GetTextWindowSize(&width, &height);
@ -857,6 +858,7 @@ void TextWindow::Paint() {
DrawOrHitTestColorPicker(&uiCanvas, PAINT, false, 0, 0); DrawOrHitTestColorPicker(&uiCanvas, PAINT, false, 0, 0);
canvas.EndFrame(); canvas.EndFrame();
#endif
} }
void TextWindow::MouseEvent(bool leftClick, bool leftDown, double x, double y) { void TextWindow::MouseEvent(bool leftClick, bool leftDown, double x, double y) {

18
test/CMakeLists.txt Normal file
View File

@ -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 $<TARGET_FILE:solvespace_testsuite>
COMMENT "Testing SolveSpace"
VERBATIM)

320
test/harness.cpp Normal file
View File

@ -0,0 +1,320 @@
//-----------------------------------------------------------------------------
// Our harness for running test cases, and reusable checks.
//
// Copyright 2016 whitequark
//-----------------------------------------------------------------------------
#include "harness.h"
#include <regex>
#if defined(WIN32)
#include <windows.h>
#else
#include <unistd.h>
#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<Pixmap> 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<uint8_t> ReadFile(std::string path) {
std::vector<uint8_t> 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<uint8_t> 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<Pixmap> 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<Pixmap> 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<Test::Case> *testCasesPtr;
int Test::Case::Register(Test::Case testCase) {
static std::vector<Test::Case> 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<double> 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;
}
}

57
test/harness.h Normal file
View File

@ -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<void(Helper *)> 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -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

View File

@ -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

View File

@ -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");
}