From ba812e52bd581d2dd17bc095dc9b4f8d79c78f42 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Mon, 13 May 2013 16:57:00 -0400 Subject: [PATCH 01/66] initial travis test --- .travis.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..e4f9ff74 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: cpp +compiler: + - gcc + - clang +before_install: + - sudu apt-get update -qq + - sudu apt-get install -qq build-essential libqt4-dev libqt4-opengl-dev libxmu-dev cmake bison flex git-core libboost-all-dev libXi-dev libmpfr-dev libboost-dev libglew-dev libeigen2-dev libeigen3-dev libcgal-dev libopencsg-dev libgmp3-dev libgmp-dev python-paramiko curl imagemagick + +branches: + only: + - travis + +script: echo "Success" From 1f6c61d9ce6baa31f86e0151519acd7df5f39ee8 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Mon, 13 May 2013 17:00:13 -0400 Subject: [PATCH 02/66] typo --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index e4f9ff74..04d47280 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,8 @@ compiler: - gcc - clang before_install: - - sudu apt-get update -qq - - sudu apt-get install -qq build-essential libqt4-dev libqt4-opengl-dev libxmu-dev cmake bison flex git-core libboost-all-dev libXi-dev libmpfr-dev libboost-dev libglew-dev libeigen2-dev libeigen3-dev libcgal-dev libopencsg-dev libgmp3-dev libgmp-dev python-paramiko curl imagemagick + - sudo apt-get update -qq + - sudo apt-get install -qq build-essential libqt4-dev libqt4-opengl-dev libxmu-dev cmake bison flex git-core libboost-all-dev libXi-dev libmpfr-dev libboost-dev libglew-dev libeigen2-dev libeigen3-dev libcgal-dev libopencsg-dev libgmp3-dev libgmp-dev python-paramiko curl imagemagick branches: only: From cc1688cab89f1be54ff7cbd7f749033d4f26d44b Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Mon, 13 May 2013 17:04:54 -0400 Subject: [PATCH 03/66] Script for kicking off Travis CI --- .travis.yml | 2 +- scripts/travis-ci.sh | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100755 scripts/travis-ci.sh diff --git a/.travis.yml b/.travis.yml index 04d47280..786e1a84 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,4 +10,4 @@ branches: only: - travis -script: echo "Success" +script: ./scripts/travis-ci.sh diff --git a/scripts/travis-ci.sh b/scripts/travis-ci.sh new file mode 100755 index 00000000..a5ea97ff --- /dev/null +++ b/scripts/travis-ci.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +qmake +make +cd tests +cmake . +make +ctest From f8abd39095b0f75aa1f94db3c6e2ecd29bcd631f Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Mon, 13 May 2013 17:16:21 -0400 Subject: [PATCH 04/66] Get OpenCSG from PPA --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 786e1a84..2d526e98 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,9 @@ compiler: - clang before_install: - sudo apt-get update -qq - - sudo apt-get install -qq build-essential libqt4-dev libqt4-opengl-dev libxmu-dev cmake bison flex git-core libboost-all-dev libXi-dev libmpfr-dev libboost-dev libglew-dev libeigen2-dev libeigen3-dev libcgal-dev libopencsg-dev libgmp3-dev libgmp-dev python-paramiko curl imagemagick + - sudo apt-get install -qq build-essential libqt4-dev libqt4-opengl-dev libxmu-dev cmake bison flex git-core libboost-all-dev libXi-dev libmpfr-dev libboost-dev libglew-dev libeigen2-dev libeigen3-dev libcgal-dev libgmp3-dev libgmp-dev python-paramiko curl imagemagick + - echo 'yes' | sudo add-apt-repository ppa:chrysn/openscad + - sudo apt-get install -qq libopencsg-dev branches: only: From e06dbbe7421ac2f7bd2f59710cae344665cd1bd7 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Mon, 13 May 2013 17:22:22 -0400 Subject: [PATCH 05/66] Get OpenCSG from PPA - attempt 2 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2d526e98..040c5dce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ compiler: before_install: - sudo apt-get update -qq - sudo apt-get install -qq build-essential libqt4-dev libqt4-opengl-dev libxmu-dev cmake bison flex git-core libboost-all-dev libXi-dev libmpfr-dev libboost-dev libglew-dev libeigen2-dev libeigen3-dev libcgal-dev libgmp3-dev libgmp-dev python-paramiko curl imagemagick - - echo 'yes' | sudo add-apt-repository ppa:chrysn/openscad + - echo 'yes' | sudo add-apt-repository ppa:chrysn/opencsg - sudo apt-get install -qq libopencsg-dev branches: From acdf95002f919186f823effc75932814bc37eb64 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Mon, 13 May 2013 17:32:53 -0400 Subject: [PATCH 06/66] Get OpenCSG from PPA - attempt 3 --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 040c5dce..f8c3fa79 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,8 @@ compiler: before_install: - sudo apt-get update -qq - sudo apt-get install -qq build-essential libqt4-dev libqt4-opengl-dev libxmu-dev cmake bison flex git-core libboost-all-dev libXi-dev libmpfr-dev libboost-dev libglew-dev libeigen2-dev libeigen3-dev libcgal-dev libgmp3-dev libgmp-dev python-paramiko curl imagemagick - - echo 'yes' | sudo add-apt-repository ppa:chrysn/opencsg - - sudo apt-get install -qq libopencsg-dev + - echo 'yes' | sudo add-apt-repository ppa:chrysn/openscad + - sudo apt-get update -qq libopencsg-dev branches: only: From 2b966f92f247d50f2b3201d492ebc167302fc1bf Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Mon, 13 May 2013 18:22:06 -0400 Subject: [PATCH 07/66] Get OpenCSG from PPA - attempt 4 --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index f8c3fa79..7c9bdd27 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,10 +3,10 @@ compiler: - gcc - clang before_install: + - echo 'yes' | sudo add-apt-repository ppa:chrysn/openscad - sudo apt-get update -qq - sudo apt-get install -qq build-essential libqt4-dev libqt4-opengl-dev libxmu-dev cmake bison flex git-core libboost-all-dev libXi-dev libmpfr-dev libboost-dev libglew-dev libeigen2-dev libeigen3-dev libcgal-dev libgmp3-dev libgmp-dev python-paramiko curl imagemagick - - echo 'yes' | sudo add-apt-repository ppa:chrysn/openscad - - sudo apt-get update -qq libopencsg-dev + - sudo apt-get install -qq libopencsg-dev branches: only: From d37cb95e2ede85193e3f3f5004095964cedc0d35 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Mon, 13 May 2013 19:32:54 -0400 Subject: [PATCH 08/66] Detect errors mid-way into test run --- scripts/travis-ci.sh | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/scripts/travis-ci.sh b/scripts/travis-ci.sh index a5ea97ff..8aa5d1ef 100755 --- a/scripts/travis-ci.sh +++ b/scripts/travis-ci.sh @@ -1,8 +1,23 @@ #!/bin/bash -qmake -make +qmake && make +if [[ $? != 0 ]]; then + echo "Error building OpenSCAD executable" + exit 1 +fi cd tests -cmake . +cmake . +if [[ $? != 0 ]]; then + echo "Error configuring test suite" + exit 1 +fi make +if [[ $? != 0 ]]; then + echo "Error building test suite" + exit 1 +fi ctest +if [[ $? != 0 ]]; then + echo "Test failure" + exit 1 +fi From 7d667b1025da27902f17dea7a85c55c830050c21 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Mon, 13 May 2013 21:53:20 -0300 Subject: [PATCH 09/66] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c22871a9..293efbbb 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +![Travis CI](https://api.travis-ci.org/openscad/openscad.png) + # What is OpenSCAD? [![Flattr this git repo](http://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=openscad&url=http://openscad.org&title=OpenSCAD&language=&tags=github&category=software) From 9e55b312651f9ef94e91ea5b0fa58fff44fe17fe Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Mon, 13 May 2013 21:14:24 -0400 Subject: [PATCH 10/66] clang not yet supported by the travis script --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7c9bdd27..9442ca4f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: cpp compiler: - gcc - - clang before_install: - echo 'yes' | sudo add-apt-repository ppa:chrysn/openscad - sudo apt-get update -qq From 83dc80ff5f1dbf30fff8d7f263d47d7f70832bd4 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Mon, 13 May 2013 21:14:34 -0400 Subject: [PATCH 11/66] Parallel build --- scripts/travis-ci.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/travis-ci.sh b/scripts/travis-ci.sh index 8aa5d1ef..6f9868df 100755 --- a/scripts/travis-ci.sh +++ b/scripts/travis-ci.sh @@ -1,6 +1,6 @@ #!/bin/bash -qmake && make +qmake && make -j4 if [[ $? != 0 ]]; then echo "Error building OpenSCAD executable" exit 1 @@ -11,12 +11,12 @@ if [[ $? != 0 ]]; then echo "Error configuring test suite" exit 1 fi -make +make -j4 if [[ $? != 0 ]]; then echo "Error building test suite" exit 1 fi -ctest +ctest -j8 if [[ $? != 0 ]]; then echo "Test failure" exit 1 From 435e0c021c5018ee5de69d3218c3e31c8ab75be5 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Wed, 30 Oct 2013 22:39:18 -0400 Subject: [PATCH 12/66] Limit parallel builds as Travis apparently runs out of memory --- scripts/travis-ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/travis-ci.sh b/scripts/travis-ci.sh index 6f9868df..9d4258ae 100755 --- a/scripts/travis-ci.sh +++ b/scripts/travis-ci.sh @@ -11,7 +11,7 @@ if [[ $? != 0 ]]; then echo "Error configuring test suite" exit 1 fi -make -j4 +make -j2 if [[ $? != 0 ]]; then echo "Error building test suite" exit 1 From cf9f19818ca5886275019f8e93c7fb8ec0e4bde6 Mon Sep 17 00:00:00 2001 From: Don Bright Date: Tue, 26 Nov 2013 20:04:57 -0600 Subject: [PATCH 13/66] prevent crash in CGAL nef3. fix #issue 410 . also deal w qmake bug re .h files --- openscad.pro | 2 ++ src/CGALEvaluator.cc | 16 +++++++++++++--- src/CGAL_Nef_polyhedron.cc | 17 ++++++++++++++--- src/cgal.h | 8 +++++--- src/export.cc | 10 +++++++++- 5 files changed, 43 insertions(+), 10 deletions(-) diff --git a/openscad.pro b/openscad.pro index fd9f4947..b38419ef 100644 --- a/openscad.pro +++ b/openscad.pro @@ -39,6 +39,7 @@ debug: DEFINES += DEBUG TEMPLATE = app INCLUDEPATH += src +DEPENDPATH += src # Handle custom library location. # Used when manually installing 3rd party libraries @@ -368,6 +369,7 @@ HEADERS += src/cgal.h \ src/PolySetCGALEvaluator.h \ src/CGALRenderer.h \ src/CGAL_Nef_polyhedron.h \ + src/CGAL_Nef3_workaround.h \ src/cgalworker.h SOURCES += src/cgalutils.cc \ diff --git a/src/CGALEvaluator.cc b/src/CGALEvaluator.cc index ec013153..242fe0f9 100644 --- a/src/CGALEvaluator.cc +++ b/src/CGALEvaluator.cc @@ -159,9 +159,19 @@ CGAL_Nef_polyhedron CGALEvaluator::applyHull(const CgaladvNode &node) PRINT("Hull() currently requires a valid 2-manifold. Please modify your design. See http://en.wikibooks.org/wiki/OpenSCAD_User_Manual/STL_Import_and_Export"); } else { - chN.p3->convert_to_Polyhedron(P); - std::transform(P.vertices_begin(), P.vertices_end(), std::back_inserter(points3d), - boost::bind(static_cast(&CGAL_Polyhedron::Vertex::point), _1)); + bool err = false; + try{ + err = nefworkaround::convert_to_Polyhedron( *(chN.p3), P ); + //chN.p3->convert_to_Polyhedron(P); + } catch (...) { + err = true; + } + if (err) { + PRINT("ERROR: CGAL NefPolyhedron->Polyhedron conversion failed"); + } else { + std::transform(P.vertices_begin(), P.vertices_end(), std::back_inserter(points3d), + boost::bind(static_cast(&CGAL_Polyhedron::Vertex::point), _1)); + } } } chnode->progress_report(); diff --git a/src/CGAL_Nef_polyhedron.cc b/src/CGAL_Nef_polyhedron.cc index 440f4edd..4761d265 100644 --- a/src/CGAL_Nef_polyhedron.cc +++ b/src/CGAL_Nef_polyhedron.cc @@ -98,11 +98,22 @@ PolySet *CGAL_Nef_polyhedron::convertToPolyset() CGAL::Failure_behaviour old_behaviour = CGAL::set_error_behaviour(CGAL::THROW_EXCEPTION); try { CGAL_Polyhedron P; - this->p3->convert_to_Polyhedron(P); - ps = createPolySetFromPolyhedron(P); + bool err = nefworkaround::convert_to_Polyhedron( *(this->p3), P ); + //this->p3->convert_to_Polyhedron(P); + if (err) { + PRINT("ERROR: CGAL NefPolyhedron->Polyhedron conversion failed"); + } else { + ps = createPolySetFromPolyhedron(P); + } } catch (const CGAL::Precondition_exception &e) { - PRINTB("CGAL error in CGAL_Nef_polyhedron::convertToPolyset(): %s", e.what()); + PRINTB("CGAL Precondition error in CGAL_Nef_polyhedron::convertToPolyset(): %s", e.what()); + } + catch (const CGAL::Assertion_exception &e) { + PRINTB("CGAL Assertion error in CGAL_Nef_polyhedron::convertToPolyset(): %s", e.what()); + } + catch (...) { + PRINT("CGAL unknown error in CGAL_Nef_polyhedron::convertToPolyset()"); } CGAL::set_error_behaviour(old_behaviour); } diff --git a/src/cgal.h b/src/cgal.h index 45228be1..69c8c270 100644 --- a/src/cgal.h +++ b/src/cgal.h @@ -27,6 +27,7 @@ using boost::uintmax_t; #include #include #include +#include #include #include #include @@ -48,9 +49,10 @@ typedef CGAL::Exact_predicates_exact_constructions_kernel CGAL_ExactKernel2; typedef CGAL::Polygon_2 CGAL_Poly2; typedef CGAL::Polygon_with_holes_2 CGAL_Poly2h; - //typedef CGAL::Cartesian CGAL_Kernel3; -typedef CGAL::Exact_predicates_exact_constructions_kernel CGAL_Kernel3; -typedef CGAL::Exact_predicates_exact_constructions_kernel::FT NT3; +typedef CGAL::Gmpq NT3; +typedef CGAL::Cartesian CGAL_Kernel3; +//typedef CGAL::Exact_predicates_exact_constructions_kernel::FT NT3; +//typedef CGAL::Exact_predicates_exact_constructions_kernel CGAL_Kernel3; typedef CGAL::Nef_polyhedron_3 CGAL_Nef_polyhedron3; typedef CGAL_Nef_polyhedron3::Aff_transformation_3 CGAL_Aff_transformation; diff --git a/src/export.cc b/src/export.cc index ec6e576a..cef323e8 100644 --- a/src/export.cc +++ b/src/export.cc @@ -42,7 +42,12 @@ void export_stl(CGAL_Nef_polyhedron *root_N, std::ostream &output) CGAL::Failure_behaviour old_behaviour = CGAL::set_error_behaviour(CGAL::THROW_EXCEPTION); try { CGAL_Polyhedron P; - root_N->p3->convert_to_Polyhedron(P); + //root_N->p3->convert_to_Polyhedron(P); + bool err = nefworkaround::convert_to_Polyhedron( *(root_N->p3), P ); + if (err) { + PRINT("ERROR: CGAL NefPolyhedron->Polyhedron conversion failed"); + return; + } typedef CGAL_Polyhedron::Vertex Vertex; typedef CGAL_Polyhedron::Vertex_const_iterator VCI; @@ -114,6 +119,9 @@ void export_stl(CGAL_Nef_polyhedron *root_N, std::ostream &output) catch (const CGAL::Assertion_exception &e) { PRINTB("CGAL error in CGAL_Nef_polyhedron3::convert_to_Polyhedron(): %s", e.what()); } + catch (...) { + PRINT("CGAL unknown error in CGAL_Nef_polyhedron3::convert_to_Polyhedron()"); + } CGAL::set_error_behaviour(old_behaviour); } From faf008ce24e5169dcfe75d90bfbc988abdfd7f93 Mon Sep 17 00:00:00 2001 From: Don Bright Date: Tue, 26 Nov 2013 20:29:29 -0600 Subject: [PATCH 14/66] simplify nef polyhedron code. attempt to add test for bug --- src/CGAL_Nef_polyhedron.cc | 26 ++++++++++++-------------- tests/CMakeLists.txt | 1 + 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/CGAL_Nef_polyhedron.cc b/src/CGAL_Nef_polyhedron.cc index 4761d265..49b9a534 100644 --- a/src/CGAL_Nef_polyhedron.cc +++ b/src/CGAL_Nef_polyhedron.cc @@ -96,24 +96,22 @@ PolySet *CGAL_Nef_polyhedron::convertToPolyset() } else if (this->dim == 3) { CGAL::Failure_behaviour old_behaviour = CGAL::set_error_behaviour(CGAL::THROW_EXCEPTION); + bool err = true; + std::string errmsg(""); + CGAL_Polyhedron P; try { - CGAL_Polyhedron P; - bool err = nefworkaround::convert_to_Polyhedron( *(this->p3), P ); + err = nefworkaround::convert_to_Polyhedron( *(this->p3), P ); //this->p3->convert_to_Polyhedron(P); - if (err) { - PRINT("ERROR: CGAL NefPolyhedron->Polyhedron conversion failed"); - } else { - ps = createPolySetFromPolyhedron(P); - } } - catch (const CGAL::Precondition_exception &e) { - PRINTB("CGAL Precondition error in CGAL_Nef_polyhedron::convertToPolyset(): %s", e.what()); + catch (const CGAL::Failure_exception &e) { + err = true; + errmsg = std::string(e.what()); } - catch (const CGAL::Assertion_exception &e) { - PRINTB("CGAL Assertion error in CGAL_Nef_polyhedron::convertToPolyset(): %s", e.what()); - } - catch (...) { - PRINT("CGAL unknown error in CGAL_Nef_polyhedron::convertToPolyset()"); + if (err) { + PRINT("ERROR: CGAL NefPolyhedron->Polyhedron conversion failed."); + if (errmsg!="") PRINTB("ERROR: %s",errmsg); + } else { + ps = createPolySetFromPolyhedron(P); } CGAL::set_error_behaviour(old_behaviour); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3d3aad1e..731a418a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -800,6 +800,7 @@ list(APPEND CGALPNGTEST_FILES ${FEATURES_FILES} ${SCAD_DXF_FILES} ${EXAMPLE_FILE list(APPEND CGALPNGTEST_FILES ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/include-tests.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/use-tests.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/transform-nan-inf-tests.scad + ${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/stl-cgal-convert_to_Polyhedron-crash.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/localfiles-test.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/localfiles_dir/localfiles-compatibility-test.scad) From fd715c6526e961cb7f3d6ba6a0563788d7d1674d Mon Sep 17 00:00:00 2001 From: Don Bright Date: Tue, 26 Nov 2013 20:55:26 -0600 Subject: [PATCH 15/66] finish adding new test, add png for new test --- tests/CMakeLists.txt | 5 +++++ ...cgal-convert_to_Polyhedron-crash-expected.png | Bin 0 -> 4350 bytes ...cgal-convert_to_Polyhedron-crash-expected.csg | 5 +++++ 3 files changed, 10 insertions(+) create mode 100644 tests/regression/cgalpngtest/stl-cgal-convert_to_Polyhedron-crash-expected.png create mode 100644 tests/regression/dumptest/stl-cgal-convert_to_Polyhedron-crash-expected.csg diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 731a418a..0477a45d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -794,6 +794,7 @@ list(APPEND DUMPTEST_FILES ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/escape-test ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/localfiles_dir/localfiles-compatibility-test.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/allexpressions.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/allfunctions.scad + ${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/stl-cgal-convert_to_Polyhedron-crash.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/allmodules.scad) list(APPEND CGALPNGTEST_FILES ${FEATURES_FILES} ${SCAD_DXF_FILES} ${EXAMPLE_FILES}) @@ -823,6 +824,10 @@ disable_tests(openscad-csgpng_child-background) disable_tests(opencsgtest_example006 cgalpngtest_example006) disable_tests(openscad-csgpng_example006 openscad-cgalpng_example006) +# NefPolyhedron->Polyhedron conversion failures. No images for OpenCSG/Thrown +disable_tests(opencsgtest_stl-cgal-convert_to_Polyhedron-crash) +disable_tests(throwntogethertest_stl-cgal-convert_to_Polyhedron-crash) + # These tests only makes sense in OpenCSG mode disable_tests(cgalpngtest_child-background cgalpngtest_highlight-and-background-modifier diff --git a/tests/regression/cgalpngtest/stl-cgal-convert_to_Polyhedron-crash-expected.png b/tests/regression/cgalpngtest/stl-cgal-convert_to_Polyhedron-crash-expected.png new file mode 100644 index 0000000000000000000000000000000000000000..318cbaabb1c5aa09c123f8da6863298527c7242d GIT binary patch literal 4350 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 YOPsfOE;2&n5B+UezsWXd9rMh80A%9J-v9sr literal 0 HcmV?d00001 diff --git a/tests/regression/dumptest/stl-cgal-convert_to_Polyhedron-crash-expected.csg b/tests/regression/dumptest/stl-cgal-convert_to_Polyhedron-crash-expected.csg new file mode 100644 index 00000000..acad52fd --- /dev/null +++ b/tests/regression/dumptest/stl-cgal-convert_to_Polyhedron-crash-expected.csg @@ -0,0 +1,5 @@ +group() { + render(convexity = 1) { + import(file = "stl-cgal-convert_to_Polyhedron-crash.stl", layer = "", origin = [0, 0], scale = 1, convexity = 1, $fn = 0, $fa = 12, $fs = 2); + } +} From 9ea7713335122eabdd243cfcf1e5ae87a8bd23d1 Mon Sep 17 00:00:00 2001 From: Don Bright Date: Sat, 30 Nov 2013 15:43:00 -0600 Subject: [PATCH 16/66] print errmsg for applyHull. add quotes around err msg in version check. --- src/CGALEvaluator.cc | 5 +++-- src/version_check.h | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/CGALEvaluator.cc b/src/CGALEvaluator.cc index 242fe0f9..4a052745 100644 --- a/src/CGALEvaluator.cc +++ b/src/CGALEvaluator.cc @@ -160,14 +160,15 @@ CGAL_Nef_polyhedron CGALEvaluator::applyHull(const CgaladvNode &node) } else { bool err = false; + std::string errmsg(""); try{ err = nefworkaround::convert_to_Polyhedron( *(chN.p3), P ); //chN.p3->convert_to_Polyhedron(P); - } catch (...) { + catch (const CGAL::Failure_exception &e) { err = true; } if (err) { - PRINT("ERROR: CGAL NefPolyhedron->Polyhedron conversion failed"); + PRINTB("ERROR: CGAL NefPolyhedron->Polyhedron conversion failed. %s", e.what()); } else { std::transform(P.vertices_begin(), P.vertices_end(), std::back_inserter(points3d), boost::bind(static_cast(&CGAL_Polyhedron::Vertex::point), _1)); diff --git a/src/version_check.h b/src/version_check.h index fbea077f..be52e61d 100644 --- a/src/version_check.h +++ b/src/version_check.h @@ -113,7 +113,7 @@ a time, to avoid confusion. + __GNUC_MINOR__ * 100 \ + __GNUC_PATCHLEVEL__) #if GCC_VERSION == 40802 -#error OpenSCAD isn't compatible with gcc 4.8.2. Please try a different version +#error "OpenSCAD isnt compatible with gcc 4.8.2. Please try a different version" #endif #endif // OPENSCAD_SKIP_VERSION_CHECK From 791a49b9e8489818e41deae2b1d4ba2b6ff50e5f Mon Sep 17 00:00:00 2001 From: Don Bright Date: Sat, 30 Nov 2013 17:26:50 -0600 Subject: [PATCH 17/66] build bug fix --- src/CGALEvaluator.cc | 7 +- src/CGAL_Nef3_workaround.h | 352 +++++++++++++++++++++++++++++++++++++ 2 files changed, 356 insertions(+), 3 deletions(-) create mode 100644 src/CGAL_Nef3_workaround.h diff --git a/src/CGALEvaluator.cc b/src/CGALEvaluator.cc index 4a052745..86118d78 100644 --- a/src/CGALEvaluator.cc +++ b/src/CGALEvaluator.cc @@ -161,14 +161,15 @@ CGAL_Nef_polyhedron CGALEvaluator::applyHull(const CgaladvNode &node) else { bool err = false; std::string errmsg(""); - try{ + try { err = nefworkaround::convert_to_Polyhedron( *(chN.p3), P ); //chN.p3->convert_to_Polyhedron(P); - catch (const CGAL::Failure_exception &e) { + } catch (const CGAL::Failure_exception &e) { err = true; + errmsg = std::string(e.what()); } if (err) { - PRINTB("ERROR: CGAL NefPolyhedron->Polyhedron conversion failed. %s", e.what()); + PRINTB("ERROR: CGAL NefPolyhedron->Polyhedron conversion failed. %s", errmsg); } else { std::transform(P.vertices_begin(), P.vertices_end(), std::back_inserter(points3d), boost::bind(static_cast(&CGAL_Polyhedron::Vertex::point), _1)); diff --git a/src/CGAL_Nef3_workaround.h b/src/CGAL_Nef3_workaround.h new file mode 100644 index 00000000..c2482ac4 --- /dev/null +++ b/src/CGAL_Nef3_workaround.h @@ -0,0 +1,352 @@ +// Copyright (c) 1997-2002,2005 Max-Planck-Institute Saarbruecken (Germany). +// All rights reserved. +// +// This file is part of CGAL (www.cgal.org). +// You can redistribute it and/or modify it under the terms of the GNU +// General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Licensees holding a valid commercial license may use this file in +// accordance with the commercial license agreement provided with the software. +// +// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +// WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +// +// $URL: svn+ssh://scm.gforge.inria.fr/svn/cgal/branches/releases/CGAL-4.0-branch/Nef_3/include/CGAL/Nef_helper_3.h $ +// $Id: Nef_helper_3.h 67117 2012-01-13 18:14:48Z lrineau $ +// +// +// Author(s) : Michael Seel +// Miguel Granados +// Susan Hert +// Lutz Kettner +// Ralf Osbild +// Peter Hachenberger + +/* + modified by don bright for OpenSCAD, 2013. + +This works around issue #410, where CGAL's Nef_Polyhedron3.convert_to_Polyhedron +throws an uncatchable exception, due to an CGAL_Assertion being thrown in +Polyhedron Incremental Builder's destructor while a Triangulation exception +is still active, resulting in program termination (crashing). + +The purpose here is not to improve/change the way CGAL's Nef code works, +but instead to tweak it slightly to prevent OpenSCAD from crashing. The +behavior of the code should otherwise be exactly the same as CGAL's standard +code. + +This file was created by copying three sections +from CGAL's Nef_polyhedron3.h that were protected/private: + + Triangulation_handler2 + Build_Polyhedron + convert_to_polyhedron + +Very small code changes have been made. First, there are many template +type specifiers added to enable the movement of the code to the outside +of the Nef Polyhedron class. Second, there is a try{}catch(...){} block +added in the Builder around the Triangulation code. Third, there is an error +variable added for non-Exception communication with the caller. + +Eventually, if CGAL itself is updated and the update is widely +distributed, this file may become obsolete and can be deleted from OpenSCAD + +*/ + + +#ifndef _CGAL_NEF3_WORKAROUND_H +#define _CGAL_NEF3_WORKAROUND_H + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "printutils.h" + +namespace nefworkaround { + +template +class Triangulation_handler2 { + + typedef typename CGAL::Triangulation_vertex_base_2 Vb; + typedef typename CGAL::Constrained_triangulation_face_base_2 Fb; + typedef typename CGAL::Triangulation_data_structure_2 TDS; + typedef typename CGAL::Constrained_triangulation_2 CT; + + typedef typename CT::Face_handle Face_handle; + typedef typename CT::Vertex_handle CTVertex_handle; + typedef typename CT::Finite_faces_iterator Finite_face_iterator; + typedef typename CT::Edge Edge; + CT ct; + CGAL::Unique_hash_map visited; + CGAL::Unique_hash_map ctv2v; + Finite_face_iterator fi; + typename Nef::Plane_3 supporting_plane; + + public: + Triangulation_handler2(typename Nef::Halffacet_const_handle f) : + visited(false), supporting_plane(f->plane()) { + + typename Nef::Halffacet_cycle_const_iterator fci; + for(fci=f->facet_cycles_begin(); fci!=f->facet_cycles_end(); ++fci) { + if(fci.is_shalfedge()) { + typename Nef::SHalfedge_around_facet_const_circulator sfc(fci), send(sfc); + CGAL_For_all(sfc,send) { + CGAL_NEF_TRACEN(" insert point" << sfc->source()->source()->point()); + CTVertex_handle ctv = ct.insert(sfc->source()->source()->point()); + ctv2v[ctv] = sfc->source()->source(); + } + } + } + + for(fci=f->facet_cycles_begin(); fci!=f->facet_cycles_end(); ++fci) { + if(fci.is_shalfedge()) { + typename Nef::SHalfedge_around_facet_const_circulator sfc(fci), send(sfc); + CGAL_For_all(sfc,send) { + CGAL_NEF_TRACEN(" insert constraint" << sfc->source()->source()->point() + << "->" << sfc->source()->twin()->source()->point()); + ct.insert_constraint(sfc->source()->source()->point(), + sfc->source()->twin()->source()->point()); + } + } + } + CGAL_assertion(ct.is_valid()); + + CGAL_NEF_TRACEN("number of finite triangles " << ct.number_of_faces()); + + typename CT::Face_handle infinite = ct.infinite_face(); + typename CT::Vertex_handle ctv = infinite->vertex(1); + if(ct.is_infinite(ctv)) ctv = infinite->vertex(2); + CGAL_assertion(!ct.is_infinite(ctv)); + + typename CT::Face_handle opposite; + typename CT::Face_circulator vc(ctv,infinite); + do { opposite = vc++; + } while(!ct.is_constrained(typename CT::Edge(vc,vc->index(opposite)))); + typename CT::Face_handle first = vc; + + CGAL_assertion(!ct.is_infinite(first)); + traverse_triangulation(first, first->index(opposite)); + + fi = ct.finite_faces_begin(); + } + + void traverse_triangulation(Face_handle f, int parent) { + visited[f] = true; + if(!ct.is_constrained(Edge(f,ct.cw(parent))) && !visited[f->neighbor(ct.cw(parent))]) { + Face_handle child(f->neighbor(ct.cw(parent))); + traverse_triangulation(child, child->index(f)); + } + if(!ct.is_constrained(Edge(f,ct.ccw(parent))) && !visited[f->neighbor(ct.ccw(parent))]) { + Face_handle child(f->neighbor(ct.ccw(parent))); + traverse_triangulation(child, child->index(f)); + } + } + + template + bool get_next_triangle(Triangle_3& tr) { + while(fi != ct.finite_faces_end() && visited[fi] == false) ++fi; + if(fi == ct.finite_faces_end()) return false; + tr = Triangle_3(fi->vertex(0)->point(), fi->vertex(1)->point(), fi->vertex(2)->point()); + ++fi; + return true; + } + + bool same_orientation(typename Nef::Plane_3 p1) const { + if(p1.a() != 0) + return CGAL::sign(p1.a()) == CGAL::sign(supporting_plane.a()); + if(p1.b() != 0) + return CGAL::sign(p1.b()) == CGAL::sign(supporting_plane.b()); + return CGAL::sign(p1.c()) == CGAL::sign(supporting_plane.c()); + } + + template + void handle_triangles(PIB& pib, Index& VI) { + while(fi != ct.finite_faces_end() && visited[fi] == false) ++fi; + while(fi != ct.finite_faces_end()) { + typename Nef::Plane_3 plane(fi->vertex(0)->point(), + fi->vertex(1)->point(), + fi->vertex(2)->point()); + pib.begin_facet(); + if(same_orientation(plane)) { + pib.add_vertex_to_facet(VI[ctv2v[fi->vertex(0)]]); + pib.add_vertex_to_facet(VI[ctv2v[fi->vertex(1)]]); + pib.add_vertex_to_facet(VI[ctv2v[fi->vertex(2)]]); + } else { + pib.add_vertex_to_facet(VI[ctv2v[fi->vertex(0)]]); + pib.add_vertex_to_facet(VI[ctv2v[fi->vertex(2)]]); + pib.add_vertex_to_facet(VI[ctv2v[fi->vertex(1)]]); + } + pib.end_facet(); + do { + ++fi; + } while(fi != ct.finite_faces_end() && visited[fi] == false); + } + } +}; + + + + + + + + +template +class Build_polyhedron : public CGAL::Modifier_base { +public: + bool error; // added for OpenSCAD + class Visitor { + typedef typename CGAL::Projection_traits_xy_3 XY; + typedef typename CGAL::Projection_traits_yz_3 YZ; + typedef typename CGAL::Projection_traits_xz_3 XZ; + + const CGAL::Object_index& VI; + CGAL::Polyhedron_incremental_builder_3& B; + const typename Nef::SNC_const_decorator& D; + + public: + bool error;//added for OpenSCAD + Visitor(CGAL::Polyhedron_incremental_builder_3& BB, + const typename Nef::SNC_const_decorator& sd, + CGAL::Object_index& vi) : VI(vi), B(BB), D(sd), error(false) {} + + void visit(typename Nef::Halffacet_const_handle opposite_facet) { + + CGAL_NEF_TRACEN("Build_polyhedron: visit facet " << opposite_facet->plane()); + + CGAL_assertion(Nef::Infi_box::is_standard(opposite_facet->plane())); + + typename Nef::SHalfedge_const_handle se; + typename Nef::Halffacet_cycle_const_iterator fc; + + typename Nef::Halffacet_const_handle f = opposite_facet->twin(); + + typename Nef::SHalfedge_around_facet_const_circulator + sfc1(f->facet_cycles_begin()), sfc2(sfc1); + + if(++f->facet_cycles_begin() != f->facet_cycles_end() || + ++(++(++sfc1)) != sfc2) { + typename Nef::Vector_3 orth = f->plane().orthogonal_vector(); + int c = CGAL::abs(orth[0]) > CGAL::abs(orth[1]) ? 0 : 1; + c = CGAL::abs(orth[2]) > CGAL::abs(orth[c]) ? 2 : c; + + try{ // added for OpenSCAD + if(c == 0) { + Triangulation_handler2 th(f); + th.handle_triangles(B, VI); + } else if(c == 1) { + Triangulation_handler2 th(f); + th.handle_triangles(B, VI); + } else if(c == 2) { + Triangulation_handler2 th(f); + th.handle_triangles(B, VI); + } else + CGAL_error_msg( "wrong value"); + } catch(...) { // added for OpenSCAD + PRINT("ERROR: CGAL NefPolyhedron Triangulation failed"); // added for OpenSCAD + this->error=true; //added for OpenSCAD + } // added for OpenSCAD + } else { + + B.begin_facet(); + fc = f->facet_cycles_begin(); + se = typename Nef::SHalfedge_const_handle(fc); + CGAL_assertion(se!=0); + typename Nef::SHalfedge_around_facet_const_circulator hc_start(se); + typename Nef::SHalfedge_around_facet_const_circulator hc_end(hc_start); + CGAL_For_all(hc_start,hc_end) { + CGAL_NEF_TRACEN(" add vertex " << hc_start->source()->center_vertex()->point()); + B.add_vertex_to_facet(VI[hc_start->source()->center_vertex()]); + } + B.end_facet(); + } + } + + void visit(typename Nef::SFace_const_handle) {} + void visit(typename Nef::Halfedge_const_handle) {} + void visit(typename Nef::Vertex_const_handle) {} + void visit(typename Nef::SHalfedge_const_handle) {} + void visit(typename Nef::SHalfloop_const_handle) {} + }; + + public: + + const typename Nef::SNC_const_decorator& scd; + CGAL::Object_index VI; + + Build_polyhedron(const typename Nef::SNC_const_decorator& s) : error(false), + scd(s), VI(s.vertices_begin(),s.vertices_end(),'V') {} + + void operator()( HDS& hds) { + CGAL::Polyhedron_incremental_builder_3 B(hds, true); + + int skip_volumes; + if(Nef::Infi_box::extended_kernel()) { + B.begin_surface(scd.number_of_vertices()-8, + scd.number_of_facets()-6, + scd.number_of_edges()-12); + skip_volumes = 2; + } + else { + B.begin_surface(scd.number_of_vertices(), + 2*scd.number_of_vertices()-4, + 3*scd.number_of_vertices()-6); + skip_volumes = 1; + } + + int vertex_index = 0; + typename Nef::Vertex_const_iterator v; + CGAL_forall_vertices(v,scd) { + if(Nef::Infi_box::is_standard(v->point())) { + VI[v]=vertex_index++; + B.add_vertex(v->point()); + } + } + + Visitor V(B,scd,VI); + typename Nef::Volume_const_handle c; + CGAL_forall_volumes(c,scd) + if(skip_volumes-- <= 0) { + scd.visit_shell_objects(typename Nef:: SFace_const_handle(c->shells_begin()),V); + } + B.end_surface(); + this->error=B.error()||V.error; // added for OpenSCAD + if (B.error()) B.rollback(); // added for OpenSCAD + } + +}; + +template +bool convert_to_Polyhedron( const CGAL::Nef_polyhedron_3 &N, CGAL::Polyhedron_3 &P ) +{ + // several lines here added for OpenSCAD + typedef typename CGAL::Nef_polyhedron_3 Nef3; + typedef typename CGAL::Polyhedron_3 Polyhedron; + typedef typename Polyhedron::HalfedgeDS HalfedgeDS; + CGAL_precondition(N.is_simple()); + P.clear(); + Build_polyhedron bp(N); + P.delegate(bp); + return bp.error; +} + + + + + +} //namespace nefworkaround + + + + +#endif + From 69cbb17497ab52620e39874a2323db1aadf6dcbe Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Wed, 4 Dec 2013 01:15:19 -0500 Subject: [PATCH 18/66] Killed warnings --- src/CocoaUtils.mm | 4 ++-- src/modcontext.cc | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CocoaUtils.mm b/src/CocoaUtils.mm index 92640fdd..9856b3d7 100644 --- a/src/CocoaUtils.mm +++ b/src/CocoaUtils.mm @@ -8,7 +8,7 @@ void CocoaUtils::endApplication() object:nil]; } -void CocoaUtils::nslog(const std::string &str, void *userdata) +void CocoaUtils::nslog(const std::string &str, void * /* userdata */) { - NSLog([NSString stringWithUTF8String: str.c_str()]); + NSLog(@"%s", str.c_str()); } diff --git a/src/modcontext.cc b/src/modcontext.cc index 5b480097..7941cf5c 100644 --- a/src/modcontext.cc +++ b/src/modcontext.cc @@ -162,7 +162,7 @@ void ModuleContext::dump(const AbstractModule *mod, const ModuleInstantiation *i #endif FileContext::FileContext(const class FileModule &module, const Context *parent) - : usedlibs(module.usedlibs), ModuleContext(parent) + : ModuleContext(parent), usedlibs(module.usedlibs) { if (!module.modulePath().empty()) this->document_path = module.modulePath(); } From f2fe074e1d947f74e34833453cc613e46e5450a6 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Wed, 4 Dec 2013 01:15:34 -0500 Subject: [PATCH 19/66] clang fix: Clang claims to be gcc --- src/stl-utils.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stl-utils.cc b/src/stl-utils.cc index 790fd179..027339cc 100644 --- a/src/stl-utils.cc +++ b/src/stl-utils.cc @@ -1,4 +1,4 @@ -#if defined(__APPLE__) && defined(__GNUC__) +#if defined(__APPLE__) && defined(__GNUC__) && !defined(__clang__) #include From d3b82dcac0cbd6bb46c3236d1183f84b76b44748 Mon Sep 17 00:00:00 2001 From: Brody Kenrick Date: Thu, 5 Dec 2013 15:56:50 +1100 Subject: [PATCH 20/66] Fix for bad boost libraries Get this error because of a search for a non-existent library on linux64 ----------- [ 69%] Built target tests-cgal Scanning dependencies of target cgalcachetest [ 70%] Building CXX object CMakeFiles/cgalcachetest.dir/cgalcachetest.cc.o make[2]: *** No rule to make target `/usr/lib/libboost_thread.so', needed by `cgalcachetest'. Stop. make[1]: *** [CMakeFiles/cgalcachetest.dir/all] Error 2 make: *** [all] Error 2 [2]+ Done gedit openscad.pro (wd: ~/git/openscad_unicode) ---------- --- tests/CMakeLists.txt | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0477a45d..4cc86f54 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -377,11 +377,45 @@ if("${CGAL_MAJOR_VERSION}.${CGAL_MINOR_VERSION}" VERSION_LESS 3.6) endif() inclusion(CGAL_DIR CGAL_INCLUDE_DIRS) +#Get rid of bad libraries suggested for BOOST dependencies (they don't exist on some machines and cause build failures). +#/usr/lib/libboost_thread.so;/usr/lib/libboost_system.so; +string(FIND "${CGAL_3RD_PARTY_LIBRARIES}" "/usr/lib/libboost_system.so" FIND_POSITION ) +if(NOT "-1" STREQUAL ${FIND_POSITION} ) +if(NOT EXISTS "/usr/lib/libboost_system.so") + MESSAGE( WARNING "CGAL_3RD_PARTY_LIBRARIES:Found erroneous /usr/lib/libboost_system.so -- stripping" ) + string(REPLACE "/usr/lib/libboost_system.so" "" CGAL_3RD_PARTY_LIBRARIES ${CGAL_3RD_PARTY_LIBRARIES}) +endif() +endif() +string(FIND "${CGAL_3RD_PARTY_LIBRARIES}" "/usr/lib/libboost_thread.so" FIND_POSITION ) +if(NOT "-1" STREQUAL ${FIND_POSITION} ) +if(NOT EXISTS "/usr/lib/libboost_thread.so") + MESSAGE( WARNING "CGAL_3RD_PARTY_LIBRARIES:Found erroneous /usr/lib/libboost_thread.so -- stripping" ) + string(REPLACE "/usr/lib/libboost_thread.so" "" CGAL_3RD_PARTY_LIBRARIES ${CGAL_3RD_PARTY_LIBRARIES}) +endif() +endif() if(${CMAKE_CXX_COMPILER} MATCHES ".*clang.*" AND NOT ${CGAL_CXX_FLAGS_INIT} STREQUAL "" ) string(REPLACE "-frounding-math" "" CGAL_CXX_FLAGS_INIT ${CGAL_CXX_FLAGS_INIT}) string(REPLACE "--param=ssp-buffer-size=4" "" CGAL_CXX_FLAGS_INIT ${CGAL_CXX_FLAGS_INIT}) endif() +if (NOT $ENV{OPENSCAD_LIBRARIES} STREQUAL "") + # Force pkg-config to look _only_ in the local library folder + # in case OPENSCAD_LIBRARIES is set. + set(ENV{PKG_CONFIG_PATH} "$ENV{OPENSCAD_LIBRARIES}/lib/pkgconfig") + set(ENV{PKG_CONFIG_LIBDIR} "$ENV{OPENSCAD_LIBRARIES}/lib/pkgconfig") +endif() + +# Find libraries (system installed or dependency built) using pkg-config +find_package(PkgConfig REQUIRED) + +#GLib-2 +pkg_search_module(GLIB2 REQUIRED glib-2.0>=2.2.0) +#Can't use the CXXFlags directly as they are ;-separated +string(REPLACE ";" " " GLIB2_CFLAGS "${GLIB2_CFLAGS}") +message(STATUS "glib-2.0 found: ${GLIB2_VERSION}") + +add_definitions(${GLIB2_CFLAGS}) + # Imagemagick if (SKIP_IMAGEMAGICK) From 0717c67c9fa894ecb08dc5de281753a00922d1ee Mon Sep 17 00:00:00 2001 From: Brody Kenrick Date: Thu, 5 Dec 2013 17:56:54 +1100 Subject: [PATCH 21/66] Unicode support for strings Add suport for using unicode strings in .scad files. Support iterating across them/accessing them via [] and searching. -------- Add GLIB (to build for test and normal build -- both with installed and built locally development files). Add support for unicode chars to length and search builtin functions and [] for strings. Added unicode testing functions. Ad GLIB to library info page. --- .gitignore | 1 + README.md | 1 + common.pri | 1 + glib-2.0.pri | 38 ++++++ openscad.pro | 3 +- scripts/check-dependencies.sh | 17 ++- scripts/uni-build-dependencies.sh | 1 + scripts/uni-get-dependencies.sh | 15 +-- src/AboutDialog.html | 1 + src/PlatformUtils.cc | 3 + src/func.cc | 49 ++++++-- src/value.cc | 18 ++- testdata/scad/misc/search-tests-unicode.scad | 116 ++++++++++++++++++ testdata/scad/misc/string-unicode.scad | 36 ++++++ tests/CMakeLists.txt | 6 +- .../search-tests-unicode-expected.echo | 109 ++++++++++++++++ .../echotest/string-unicode-expected.echo | 104 ++++++++++++++++ 17 files changed, 497 insertions(+), 22 deletions(-) create mode 100644 glib-2.0.pri create mode 100644 testdata/scad/misc/search-tests-unicode.scad create mode 100644 testdata/scad/misc/string-unicode.scad create mode 100644 tests/regression/echotest/search-tests-unicode-expected.echo create mode 100644 tests/regression/echotest/string-unicode-expected.echo diff --git a/.gitignore b/.gitignore index 50dace12..59bac496 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /*.scad *.dmg +*~ *.tar* Makefile objects diff --git a/README.md b/README.md index 27f12cec..1e97e0f6 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ Follow the instructions for the platform you're compiling on below. * [OpenCSG (1.3.2)](http://www.opencsg.org/) * [GLEW (1.5.4 ->)](http://glew.sourceforge.net/) * [Eigen (3.0 - 3.2)](http://eigen.tuxfamily.org/) +* [glib2 (2.2.0)](https://developer.gnome.org/glib/) * [GCC C++ Compiler (4.2 ->)](http://gcc.gnu.org/) * [Bison (2.4)](http://www.gnu.org/software/bison/) * [Flex (2.5.35)](http://flex.sourceforge.net/) diff --git a/common.pri b/common.pri index 7153ded7..696c8b1d 100644 --- a/common.pri +++ b/common.pri @@ -11,3 +11,4 @@ include(opencsg.pri) include(glew.pri) include(eigen.pri) include(boost.pri) +include(glib-2.0.pri) \ No newline at end of file diff --git a/glib-2.0.pri b/glib-2.0.pri new file mode 100644 index 00000000..0fbc4e2d --- /dev/null +++ b/glib-2.0.pri @@ -0,0 +1,38 @@ +# Detect glib-2.0, then use this priority list to determine +# which library to use: +# +# Priority +# 1. GLIB2_INCLUDEPATH / GLIB2_LIBPATH (qmake parameter, not checked it given on commandline) +# 2. OPENSCAD_LIBRARIES (environment variable) +# 3. system's standard include paths from pkg-config + +glib-2.0 { + +# read environment variables +OPENSCAD_LIBRARIES_DIR = $$(OPENSCAD_LIBRARIES) +GLIB2_DIR = $$(GLIB2DIR) + +!isEmpty(OPENSCAD_LIBRARIES_DIR) { + isEmpty(GLIB2_INCLUDEPATH) { + GLIB2_INCLUDEPATH_1 = $$OPENSCAD_LIBRARIES_DIR/include/glib-2.0 + GLIB2_INCLUDEPATH_2 = $$OPENSCAD_LIBRARIES_DIR/lib/glib-2.0/include + GLIB2_LIBPATH = $$OPENSCAD_LIBRARIES_DIR/lib + } +} + +isEmpty(GLIB2_INCLUDEPATH) { + GLIB2_CFLAGS = $$system("pkg-config --cflags glib-2.0") +} else { + GLIB2_CFLAGS = -I$$GLIB2_INCLUDEPATH_1 + GLIB2_CFLAGS += -I$$GLIB2_INCLUDEPATH_2 +} + +isEmpty(GLIB2_LIBPATH) { + GLIB2_LIBS = $$system("pkg-config --libs glib-2.0") +} else { + GLIB2_LIBS = -L$$GLIB2_LIBPATH -lglib-2.0 +} + +QMAKE_CXXFLAGS += $$GLIB2_CFLAGS +LIBS += $$GLIB2_LIBS +} diff --git a/openscad.pro b/openscad.pro index b38419ef..ec5af20c 100644 --- a/openscad.pro +++ b/openscad.pro @@ -8,7 +8,7 @@ # OPENCSGDIR # OPENSCAD_LIBRARIES # -# Please see the 'Buildling' sections of the OpenSCAD user manual +# Please see the 'Building' sections of the OpenSCAD user manual # for updated tips & workarounds. # # http://en.wikibooks.org/wiki/OpenSCAD_User_Manual @@ -156,6 +156,7 @@ CONFIG += cgal CONFIG += opencsg CONFIG += boost CONFIG += eigen +CONFIG += glib-2.0 #Uncomment the following line to enable QCodeEdit #CONFIG += qcodeedit diff --git a/scripts/check-dependencies.sh b/scripts/check-dependencies.sh index b63c6770..e5871989 100755 --- a/scripts/check-dependencies.sh +++ b/scripts/check-dependencies.sh @@ -66,6 +66,21 @@ cgal_sysver() cgal_sysver_result=`grep "define *CGAL_VERSION *[0-9.]*" $cgalpath | awk '{print $3}'` } +glib2_sysver() +{ + #Get architecture triplet - e.g. x86_64-linux-gnu + glib2archtriplet=`gcc -dumpmachine 2>/dev/null` + if [ -z "$VAR" ]; then + glib2archtriplet=`dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null` + fi + glib2path=$1/lib/$glib2archtriplet/glib-2.0/include/glibconfig.h + if [ ! -e $glib2path ]; then return; fi + glib2major=`grep "define *GLIB_MAJOR_VERSION *[0-9.]*" $glib2path | awk '{print $3}'` + glib2minor=`grep "define *GLIB_MINOR_VERSION *[0-9.]*" $glib2path | awk '{print $3}'` + glib2micro=`grep "define *GLIB_MICRO_VERSION *[0-9.]*" $glib2path | awk '{print $3}'` + glib2_sysver_result="${glib2major}.${glib2minor}.${glib2micro}" +} + boost_sysver() { boostpath=$1/include/boost/version.hpp @@ -530,7 +545,7 @@ checkargs() main() { - deps="qt4 cgal gmp mpfr boost opencsg glew eigen gcc bison flex make" + deps="qt4 cgal gmp mpfr boost opencsg glew eigen glib2 gcc bison flex make" #deps="$deps curl git" # not technically necessary for build #deps="$deps python cmake imagemagick" # only needed for tests #deps="cgal" diff --git a/scripts/uni-build-dependencies.sh b/scripts/uni-build-dependencies.sh index e652c473..8d912c35 100755 --- a/scripts/uni-build-dependencies.sh +++ b/scripts/uni-build-dependencies.sh @@ -603,5 +603,6 @@ build_boost 1.53.0 build_cgal 4.0.2 build_glew 1.9.0 build_opencsg 1.3.2 +build_glib2 2.38.2 echo "OpenSCAD dependencies built and installed to " $BASEDIR diff --git a/scripts/uni-get-dependencies.sh b/scripts/uni-get-dependencies.sh index a0306ef8..d2408c00 100755 --- a/scripts/uni-get-dependencies.sh +++ b/scripts/uni-get-dependencies.sh @@ -8,7 +8,7 @@ get_fedora_deps() { sudo yum install qt-devel bison flex eigen3-devel python-paramiko \ boost-devel mpfr-devel gmp-devel glew-devel CGAL-devel gcc gcc-c++ pkgconfig \ - opencsg-devel git libXmu-devel curl imagemagick ImageMagick make \ + opencsg-devel git libXmu-devel curl imagemagick ImageMagick glib2-devel make \ xorg-x11-server-Xvfb } @@ -21,7 +21,7 @@ get_altlinux_deps() { for i in boost-devel boost-filesystem-devel gcc4.5 gcc4.5-c++ boost-program_options-devel \ boost-thread-devel boost-system-devel boost-regex-devel eigen3 libmpfr libgmp libgmp_cxx-devel qt4-devel libcgal-devel git-core \ - libglew-devel flex bison curl imagemagick; do sudo apt-get install $i; done + libglew-devel flex bison curl imagemagick glib2-devel; do sudo apt-get install $i; done } get_freebsd_deps() @@ -29,20 +29,21 @@ get_freebsd_deps() pkg_add -r bison boost-libs cmake git bash eigen3 flex gmake gmp mpfr \ xorg libGLU libXmu libXi xorg-vfbserver glew \ qt4-corelib qt4-gui qt4-moc qt4-opengl qt4-qmake qt4-rcc qt4-uic \ - opencsg cgal curl imagemagick + opencsg cgal curl imagemagick glib2-devel } get_netbsd_deps() { sudo pkgin install bison boost cmake git bash eigen flex gmake gmp mpfr \ qt4 glew cgal opencsg modular-xorg python27 py27-paramiko curl \ - imagemagick ImageMagick + imagemagick ImageMagick glib2-devel } get_opensuse_deps() { sudo zypper install libeigen3-devel mpfr-devel gmp-devel boost-devel \ - libqt4-devel glew-devel cmake git bison flex cgal-devel opencsg-devel curl + libqt4-devel glew-devel cmake git bison flex cgal-devel opencsg-devel curl \ + glib2-devel } get_mageia_deps() @@ -50,7 +51,7 @@ get_mageia_deps() sudo urpmi ctags sudo urpmi task-c-devel task-c++-devel libqt4-devel libgmp-devel \ libmpfr-devel libboost-devel eigen3-devel libglew-devel bison flex \ - cmake imagemagick python curl git x11-server-xvfb + cmake imagemagick glib2-devel python curl git x11-server-xvfb } get_debian_deps() @@ -59,7 +60,7 @@ get_debian_deps() libxmu-dev cmake bison flex git-core libboost-all-dev \ libXi-dev libmpfr-dev libboost-dev libglew-dev \ libeigen3-dev libcgal-dev libopencsg-dev libgmp3-dev libgmp-dev \ - python-paramiko curl imagemagick; do + python-paramiko curl imagemagick libglib2.0-dev; do sudo apt-get -y install $pkg; done } diff --git a/src/AboutDialog.html b/src/AboutDialog.html index 99e7c3b2..65a54d71 100644 --- a/src/AboutDialog.html +++ b/src/AboutDialog.html @@ -64,6 +64,7 @@ Please visit this link for a copy of the license: C++, GCC, clang
  • python
  • Nullsoft installer +
  • GLib

    diff --git a/src/PlatformUtils.cc b/src/PlatformUtils.cc index b02b822e..8b39f6df 100644 --- a/src/PlatformUtils.cc +++ b/src/PlatformUtils.cc @@ -1,6 +1,8 @@ #include "PlatformUtils.h" #include "boosty.h" +#include + bool PlatformUtils::createLibraryPath() { std::string path = PlatformUtils::libraryPath(); @@ -114,6 +116,7 @@ std::string PlatformUtils::info() << "\nOpenCSG version: " << OPENCSG_VERSION_STRING << "\nQt version: " << qtVersion << "\nMingW build: " << mingwstatus + << "\nGLib version: " << GLIB_MAJOR_VERSION << "." << GLIB_MINOR_VERSION << "." << GLIB_MICRO_VERSION << "\nOPENSCADPATH: " << getenv("OPENSCADPATH") << "\n" ; return s.str(); diff --git a/src/func.cc b/src/func.cc index 865a2b42..4587f725 100644 --- a/src/func.cc +++ b/src/func.cc @@ -45,6 +45,8 @@ #include #include +/*Unicode support for string lengths and array accesses*/ +#include #ifdef __WIN32__ #include @@ -306,7 +308,11 @@ Value builtin_length(const Context *, const EvalContext *evalctx) { if (evalctx->numArgs() == 1) { if (evalctx->getArgValue(0).type() == Value::VECTOR) return Value(int(evalctx->getArgValue(0).toVector().size())); - if (evalctx->getArgValue(0).type() == Value::STRING) return Value(int(evalctx->getArgValue(0).toString().size())); + if (evalctx->getArgValue(0).type() == Value::STRING) { + //Unicode glyph count for the length -- rather than the string (num. of bytes) length. + std::string text = evalctx->getArgValue(0).toString(); + return Value(int( g_utf8_strlen( text.c_str(), text.size() ) )); + } } return Value(); } @@ -380,10 +386,17 @@ Value builtin_lookup(const Context *, const EvalContext *evalctx) num_returns_per_match : int; index_col_num : int; + The search string and searched strings can be unicode strings. Examples: Index values return as list: search("a","abcdabcd"); - - returns [0,4] + - returns [0] + search("Л","Л"); //A unicode string + - returns [0] + search("🂡aЛ","a🂡Л🂡a🂡Л🂡a",0); + - returns [[1,3,5,7],[0,4,8],[2,6]] + search("a","abcdabcd",0); //Search up to all matches + - returns [[0,4]] search("a","abcdabcd",1); - returns [0] search("e","abcdabcd",1); @@ -433,16 +446,25 @@ Value builtin_search(const Context *, const EvalContext *evalctx) } } else if (findThis.type() == Value::STRING) { unsigned int searchTableSize; - if (searchTable.type() == Value::STRING) searchTableSize = searchTable.toString().size(); - else searchTableSize = searchTable.toVector().size(); - for (size_t i = 0; i < findThis.toString().size(); i++) { + //Unicode glyph count for the length + unsigned int findThisSize = g_utf8_strlen( findThis.toString().c_str(), findThis.toString().size() ); + if (searchTable.type() == Value::STRING) { + searchTableSize = g_utf8_strlen( searchTable.toString().c_str(), searchTable.toString().size() ); + } else { + searchTableSize = searchTable.toVector().size(); + } + for (size_t i = 0; i < findThisSize; i++) { unsigned int matchCount = 0; Value::VectorType resultvec; for (size_t j = 0; j < searchTableSize; j++) { - if ((searchTable.type() == Value::VECTOR && - findThis.toString()[i] == searchTable.toVector()[j].toVector()[index_col_num].toString()[0]) || - (searchTable.type() == Value::STRING && - findThis.toString()[i] == searchTable.toString()[j])) { + gchar* ptr_ft = g_utf8_offset_to_pointer(findThis.toString().c_str(), i); + gchar* ptr_st = NULL; + if(searchTable.type() == Value::VECTOR) { + ptr_st = g_utf8_offset_to_pointer(searchTable.toVector()[j].toVector()[index_col_num].toString().c_str(), 0); + } else if(searchTable.type() == Value::STRING){ + ptr_st = g_utf8_offset_to_pointer(searchTable.toString().c_str(), j); + } + if( (ptr_ft) && (ptr_st) && (g_utf8_get_char(ptr_ft) == g_utf8_get_char(ptr_st)) ) { Value resultValue((double(j))); matchCount++; if (num_returns_per_match == 1) { @@ -454,7 +476,14 @@ Value builtin_search(const Context *, const EvalContext *evalctx) if (num_returns_per_match > 1 && matchCount >= num_returns_per_match) break; } } - if (matchCount == 0) PRINTB(" WARNING: search term not found: \"%s\"", findThis.toString()[i]); + if (matchCount == 0) { + gchar* ptr_ft = g_utf8_offset_to_pointer(findThis.toString().c_str(), i); + gchar utf8_of_cp[6] = ""; //A buffer for a single unicode character to be copied into + if(ptr_ft) { + g_utf8_strncpy( utf8_of_cp, ptr_ft, 1 ); + } + PRINTB(" WARNING: search term not found: \"%s\"", utf8_of_cp ); + } if (num_returns_per_match == 0 || num_returns_per_match > 1) { returnvec.push_back(Value(resultvec)); } diff --git a/src/value.cc b/src/value.cc index 5afb650c..c8a88c69 100644 --- a/src/value.cc +++ b/src/value.cc @@ -36,6 +36,8 @@ #include #include "boost-utils.h" #include "boosty.h" +/*Unicode support for string lengths and array accesses*/ +#include std::ostream &operator<<(std::ostream &stream, const Filename &filename) { @@ -579,14 +581,28 @@ Value Value::operator-() const } */ +/* + * bracket operation [] detecting multi-byte unicode. + * If the string is multi-byte unicode then the index will offset to the character (2 or 4 byte) and not to the byte. + * A 'normal' string with byte chars are a subset of unicode and still work. + */ class bracket_visitor : public boost::static_visitor { public: Value operator()(const std::string &str, const double &idx) const { int i = int(idx); Value v; + //Check that the index is positive and less than the size in bytes if ((i >= 0) && (i < (int)str.size())) { - v = Value(str[int(idx)]); + //Ensure character (not byte) index is inside the character/glyph array + if( (unsigned) i < g_utf8_strlen( str.c_str(), str.size() ) ) { + gchar utf8_of_cp[6] = ""; //A buffer for a single unicode character to be copied into + gchar* ptr = g_utf8_offset_to_pointer(str.c_str(), i); + if(ptr) { + g_utf8_strncpy(utf8_of_cp, ptr, 1); + } + v = std::string(utf8_of_cp); + } // std::cout << "bracket_visitor: " << v << "\n"; } return v; diff --git a/testdata/scad/misc/search-tests-unicode.scad b/testdata/scad/misc/search-tests-unicode.scad new file mode 100644 index 00000000..d863eff9 --- /dev/null +++ b/testdata/scad/misc/search-tests-unicode.scad @@ -0,0 +1,116 @@ +//Test search with unicode strings + +//Helper function that pretty prints our search test +//Expected result is checked against execution of a search() invocation and OK/FAIL is indicated +module test_search_and_echo( exp_res, search_to_find, search_to_search, search_up_to_num_matches = undef) +{ + if(undef != search_up_to_num_matches) + { + assign( test_res = search(search_to_find, search_to_search, search_up_to_num_matches) ) + echo(str("Expect ", exp_res, " for search(", search_to_find, ", ", search_to_search, ", ", search_up_to_num_matches, ")=", test_res, ". ", (exp_res == test_res)?"OK":"FAIL" )); + } + else + { + assign( test_res = search(search_to_find, search_to_search) ) + echo(str("Expect ", exp_res, " for search(", search_to_find, ", ", search_to_search, ")=", test_res, ". ", (exp_res == test_res)?"OK":"FAIL" )); + } +} + + +//"Normal" text for comparison +echo ("----- Lookup of 1 byte into 1 byte"); +//Hits - up_to_count 1 +test_search_and_echo( [0], "a","aaaa" ); +test_search_and_echo( [0], "a","aaaa",1 ); +test_search_and_echo( [0,0], "aa","aaaa" ); +test_search_and_echo( [0,0], "aa","aaaa",1 ); + + +//Hits - up to count 1+ (incl 0 == all) +test_search_and_echo( [[0,1,2,3]] , "a","aaaa",0 ); +test_search_and_echo( [[0,1]], "a","aaaa",2 ); +test_search_and_echo( [[0,1,2]], "a","aaaa",3 ); +test_search_and_echo( [[0,1,2,3]] , "a","aaaa",4 ); +test_search_and_echo( [[0,1,2,3],[0,1,2,3]] , "aa","aaaa",0 ); +//Misses +test_search_and_echo( [], "b","aaaa" ); +test_search_and_echo( [], "b","aaaa",1 ); +test_search_and_echo( [[]], "b","aaaa",0 ); +test_search_and_echo( [[]], "b","aaaa",2 ); + +test_search_and_echo( [], "bb","aaaa" ); +test_search_and_echo( [], "bb","aaaa",1 ); +test_search_and_echo( [[],[]], "bb","aaaa",0 ); +test_search_and_echo( [[],[]], "bb","aaaa",2 ); +//Miss - empties +test_search_and_echo( [], "","aaaa" ); +test_search_and_echo( [], "","" ); +test_search_and_echo( [], "a","" ); + + +//Unicode tests +echo ("----- Lookup of multi-byte into 1 byte"); +test_search_and_echo( [], "Л","aaaa" ); +test_search_and_echo( [], "🂡","aaaa" ); +test_search_and_echo( [[]], "Л","aaaa",0 ); +test_search_and_echo( [[]], "🂡","aaaa",0 ); + +test_search_and_echo( [], "ЛЛ","aaaa" ); +test_search_and_echo( [], "🂡🂡","aaaa" ); +test_search_and_echo( [[],[]], "ЛЛ","aaaa",0 ); +test_search_and_echo( [[],[]], "🂡🂡","aaaa",0 ); + +echo ("----- Lookup of 1-byte into multi-byte"); +test_search_and_echo( [] , "a","ЛЛЛЛ" ); +test_search_and_echo( [] , "a","🂡🂡🂡🂡" ); +test_search_and_echo( [] , "a","ЛЛЛЛ",1 ); + +test_search_and_echo( [[]] , "a","🂡🂡🂡🂡",0 ); +test_search_and_echo( [[]] , "a","🂡🂡🂡🂡",2 ); + +echo ("----- Lookup of 1-byte into mixed multi-byte"); +test_search_and_echo( [0], "a","aЛaЛaЛaЛa" ); +test_search_and_echo( [0], "a","a🂡a🂡a🂡a🂡a" ); +test_search_and_echo( [0], "a","a🂡Л🂡a🂡Л🂡a" ); + +test_search_and_echo( [[0,2,4,6,8]], "a","aЛaЛaЛaЛa",0 ); +test_search_and_echo( [[0,2,4,6,8]], "a","a🂡a🂡a🂡a🂡a", 0 ); +test_search_and_echo( [[0,4,8]] , "a","a🂡Л🂡a🂡Л🂡a", 0 ); + +echo ("----- Lookup of 2-byte into 2-byte"); +test_search_and_echo( [0] , "Л","ЛЛЛЛ" ); +test_search_and_echo( [[0,1,2,3]] , "Л","ЛЛЛЛ",0 ); + +echo ("----- Lookup of 2-byte into 4-byte"); +test_search_and_echo( [] , "Л","🂡🂡🂡🂡" ); + +echo ("----- Lookup of 4-byte into 4-byte"); +test_search_and_echo( [0] , "🂡","🂡🂡🂡🂡" ); +test_search_and_echo( [[0,1,2,3]], "🂡","🂡🂡🂡🂡",0 ); + +echo ("----- Lookup of 4-byte into 2-byte"); +test_search_and_echo( [] , "🂡","ЛЛЛЛ" ); + +echo ("----- Lookup of 2-byte into mixed multi-byte"); +test_search_and_echo( [1] , "Л","aЛaЛaЛaЛa",1 ); +test_search_and_echo( [] , "Л","a🂡a🂡a🂡a🂡a", 1 ); +test_search_and_echo( [2] , "Л","a🂡Л🂡a🂡Л🂡a", 1 ); + +test_search_and_echo( [[1,3,5,7]] , "Л","aЛaЛaЛaЛa",0 ); +test_search_and_echo( [[]] , "Л","a🂡a🂡a🂡a🂡a", 0 ); +test_search_and_echo( [[2,6]] , "Л","a🂡Л🂡a🂡Л🂡a", 0 ); + +echo ("----- Lookup of 4-byte into mixed multi-byte"); +test_search_and_echo( [] , "🂡","aЛaЛaЛaЛa",1 ); +test_search_and_echo( [1] , "🂡","a🂡a🂡a🂡a🂡a", 1 ); + +test_search_and_echo( [[]] , "🂡","aЛaЛaЛaЛa",0 ); +test_search_and_echo( [[1,3,5,7]] , "🂡","a🂡a🂡a🂡a🂡a", 0 ); +test_search_and_echo( [[1,3,5,7]] , "🂡","a🂡Л🂡a🂡Л🂡a", 0 ); + +echo ("----- Lookup of mixed multi-byte into mixed multi-byte"); +test_search_and_echo( [[0,2,4,6,8],[1,3,5,7],[]], "aЛ🂡","aЛaЛaЛaЛa",0 ); +test_search_and_echo( [[0,2,4,6,8],[],[1,3,5,7]], "aЛ🂡","a🂡a🂡a🂡a🂡a", 0 ); +test_search_and_echo( [[0,4,8],[2,6],[1,3,5,7]] , "aЛ🂡","a🂡Л🂡a🂡Л🂡a", 0 ); +test_search_and_echo( [[1,3,5,7],[0,4,8],[2,6]] , "🂡aЛ","a🂡Л🂡a🂡Л🂡a", 0 ); + diff --git a/testdata/scad/misc/string-unicode.scad b/testdata/scad/misc/string-unicode.scad new file mode 100644 index 00000000..d8e3e5c9 --- /dev/null +++ b/testdata/scad/misc/string-unicode.scad @@ -0,0 +1,36 @@ +//Test how well arrays of unicode string are accessed. + +texts_array = [ +"DEADBEEF", +"Ленивый рыжий кот", +"كسول الزنجبيل القط", +"懶惰的姜貓", +"äöü ÄÖÜ ß", +"😁😂😃😄😅😆😇😈😉😊😋😌😍😎😏😐", +"⠁⠂⠃⠄⠅⠆⠇⠈⠉⠊⠋⠌⠍⠎⠏", +"🂡🂱🃁🃑", +]; + +text_2bytes = "Ленивый рыжий кот"; +text_4bytes = "🂡🂱🃁🃑"; + + +//Test all the normal accesses +for (text_array_idx = [0:(len(texts_array)-1)]) +{ + echo( "[", text_array_idx, "] = ", texts_array[text_array_idx], " of len=", len(texts_array[text_array_idx]), ":" ); + for (text_idx = [0:(len(texts_array[text_array_idx])-1)]) + { + echo( " [", text_idx, ,"]=", texts_array[text_array_idx][text_idx] ); + } +} + +//Test one past the last element of (x-byte unicode). This will be one past the length but inside the char length of the string +echo( "Past end of unicode only 2-byte ", text_2bytes[len(text_2bytes)] ); +echo( "Past end of unicode only 4-byte ", text_4bytes[len(text_4bytes)] ); + +//Test past the last element of (x-byte unicode). Outside both lengths. +echo( "Past end of both 2-byte ", text_2bytes[ len(text_2bytes) * 2 ] ); +echo( "Past end of both 4-byte ", text_4bytes[ len(text_4bytes) * 4 ] ); + + diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4cc86f54..fd5097a8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -587,8 +587,8 @@ set(OFFSCREEN_SOURCES ../src/OpenCSGRenderer.cc) add_library(tests-core STATIC ${CORE_SOURCES}) -target_link_libraries(tests-core ${OPENGL_LIBRARIES}) -set(TESTS-CORE-LIBRARIES ${OPENGL_LIBRARIES} ${Boost_LIBRARIES}) +target_link_libraries(tests-core ${OPENGL_LIBRARIES} ${GLIB2_LIBRARIES} ) +set(TESTS-CORE-LIBRARIES ${OPENGL_LIBRARIES} ${GLIB2_LIBRARIES} ${Boost_LIBRARIES} ) add_library(tests-common STATIC ${COMMON_SOURCES}) target_link_libraries(tests-common tests-core) @@ -808,8 +808,10 @@ list(APPEND ECHO_FILES ${FUNCTION_FILES} ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/dim-all.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/string-test.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/string-indexing.scad + ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/string-unicode.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/vector-values.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/search-tests.scad + ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/search-tests-unicode.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/recursion-tests.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/value-reassignment-tests.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/value-reassignment-tests2.scad diff --git a/tests/regression/echotest/search-tests-unicode-expected.echo b/tests/regression/echotest/search-tests-unicode-expected.echo new file mode 100644 index 00000000..801bc8c8 --- /dev/null +++ b/tests/regression/echotest/search-tests-unicode-expected.echo @@ -0,0 +1,109 @@ +ECHO: "----- Lookup of 1 byte into 1 byte" +ECHO: "Expect [0] for search(a, aaaa)=[0]. OK" +ECHO: "Expect [0] for search(a, aaaa, 1)=[0]. OK" +ECHO: "Expect [0, 0] for search(aa, aaaa)=[0, 0]. OK" +ECHO: "Expect [0, 0] for search(aa, aaaa, 1)=[0, 0]. OK" +ECHO: "Expect [[0, 1, 2, 3]] for search(a, aaaa, 0)=[[0, 1, 2, 3]]. OK" +ECHO: "Expect [[0, 1]] for search(a, aaaa, 2)=[[0, 1]]. OK" +ECHO: "Expect [[0, 1, 2]] for search(a, aaaa, 3)=[[0, 1, 2]]. OK" +ECHO: "Expect [[0, 1, 2, 3]] for search(a, aaaa, 4)=[[0, 1, 2, 3]]. OK" +ECHO: "Expect [[0, 1, 2, 3], [0, 1, 2, 3]] for search(aa, aaaa, 0)=[[0, 1, 2, 3], [0, 1, 2, 3]]. OK" + WARNING: search term not found: "b" +ECHO: "Expect [] for search(b, aaaa)=[]. OK" + WARNING: search term not found: "b" +ECHO: "Expect [] for search(b, aaaa, 1)=[]. OK" + WARNING: search term not found: "b" +ECHO: "Expect [[]] for search(b, aaaa, 0)=[[]]. OK" + WARNING: search term not found: "b" +ECHO: "Expect [[]] for search(b, aaaa, 2)=[[]]. OK" + WARNING: search term not found: "b" + WARNING: search term not found: "b" +ECHO: "Expect [] for search(bb, aaaa)=[]. OK" + WARNING: search term not found: "b" + WARNING: search term not found: "b" +ECHO: "Expect [] for search(bb, aaaa, 1)=[]. OK" + WARNING: search term not found: "b" + WARNING: search term not found: "b" +ECHO: "Expect [[], []] for search(bb, aaaa, 0)=[[], []]. OK" + WARNING: search term not found: "b" + WARNING: search term not found: "b" +ECHO: "Expect [[], []] for search(bb, aaaa, 2)=[[], []]. OK" +ECHO: "Expect [] for search(, aaaa)=[]. OK" +ECHO: "Expect [] for search(, )=[]. OK" + WARNING: search term not found: "a" +ECHO: "Expect [] for search(a, )=[]. OK" +ECHO: "----- Lookup of multi-byte into 1 byte" + WARNING: search term not found: "Л" +ECHO: "Expect [] for search(Л, aaaa)=[]. OK" + WARNING: search term not found: "🂡" +ECHO: "Expect [] for search(🂡, aaaa)=[]. OK" + WARNING: search term not found: "Л" +ECHO: "Expect [[]] for search(Л, aaaa, 0)=[[]]. OK" + WARNING: search term not found: "🂡" +ECHO: "Expect [[]] for search(🂡, aaaa, 0)=[[]]. OK" + WARNING: search term not found: "Л" + WARNING: search term not found: "Л" +ECHO: "Expect [] for search(ЛЛ, aaaa)=[]. OK" + WARNING: search term not found: "🂡" + WARNING: search term not found: "🂡" +ECHO: "Expect [] for search(🂡🂡, aaaa)=[]. OK" + WARNING: search term not found: "Л" + WARNING: search term not found: "Л" +ECHO: "Expect [[], []] for search(ЛЛ, aaaa, 0)=[[], []]. OK" + WARNING: search term not found: "🂡" + WARNING: search term not found: "🂡" +ECHO: "Expect [[], []] for search(🂡🂡, aaaa, 0)=[[], []]. OK" +ECHO: "----- Lookup of 1-byte into multi-byte" + WARNING: search term not found: "a" +ECHO: "Expect [] for search(a, ЛЛЛЛ)=[]. OK" + WARNING: search term not found: "a" +ECHO: "Expect [] for search(a, 🂡🂡🂡🂡)=[]. OK" + WARNING: search term not found: "a" +ECHO: "Expect [] for search(a, ЛЛЛЛ, 1)=[]. OK" + WARNING: search term not found: "a" +ECHO: "Expect [[]] for search(a, 🂡🂡🂡🂡, 0)=[[]]. OK" + WARNING: search term not found: "a" +ECHO: "Expect [[]] for search(a, 🂡🂡🂡🂡, 2)=[[]]. OK" +ECHO: "----- Lookup of 1-byte into mixed multi-byte" +ECHO: "Expect [0] for search(a, aЛaЛaЛaЛa)=[0]. OK" +ECHO: "Expect [0] for search(a, a🂡a🂡a🂡a🂡a)=[0]. OK" +ECHO: "Expect [0] for search(a, a🂡Л🂡a🂡Л🂡a)=[0]. OK" +ECHO: "Expect [[0, 2, 4, 6, 8]] for search(a, aЛaЛaЛaЛa, 0)=[[0, 2, 4, 6, 8]]. OK" +ECHO: "Expect [[0, 2, 4, 6, 8]] for search(a, a🂡a🂡a🂡a🂡a, 0)=[[0, 2, 4, 6, 8]]. OK" +ECHO: "Expect [[0, 4, 8]] for search(a, a🂡Л🂡a🂡Л🂡a, 0)=[[0, 4, 8]]. OK" +ECHO: "----- Lookup of 2-byte into 2-byte" +ECHO: "Expect [0] for search(Л, ЛЛЛЛ)=[0]. OK" +ECHO: "Expect [[0, 1, 2, 3]] for search(Л, ЛЛЛЛ, 0)=[[0, 1, 2, 3]]. OK" +ECHO: "----- Lookup of 2-byte into 4-byte" + WARNING: search term not found: "Л" +ECHO: "Expect [] for search(Л, 🂡🂡🂡🂡)=[]. OK" +ECHO: "----- Lookup of 4-byte into 4-byte" +ECHO: "Expect [0] for search(🂡, 🂡🂡🂡🂡)=[0]. OK" +ECHO: "Expect [[0, 1, 2, 3]] for search(🂡, 🂡🂡🂡🂡, 0)=[[0, 1, 2, 3]]. OK" +ECHO: "----- Lookup of 4-byte into 2-byte" + WARNING: search term not found: "🂡" +ECHO: "Expect [] for search(🂡, ЛЛЛЛ)=[]. OK" +ECHO: "----- Lookup of 2-byte into mixed multi-byte" +ECHO: "Expect [1] for search(Л, aЛaЛaЛaЛa, 1)=[1]. OK" + WARNING: search term not found: "Л" +ECHO: "Expect [] for search(Л, a🂡a🂡a🂡a🂡a, 1)=[]. OK" +ECHO: "Expect [2] for search(Л, a🂡Л🂡a🂡Л🂡a, 1)=[2]. OK" +ECHO: "Expect [[1, 3, 5, 7]] for search(Л, aЛaЛaЛaЛa, 0)=[[1, 3, 5, 7]]. OK" + WARNING: search term not found: "Л" +ECHO: "Expect [[]] for search(Л, a🂡a🂡a🂡a🂡a, 0)=[[]]. OK" +ECHO: "Expect [[2, 6]] for search(Л, a🂡Л🂡a🂡Л🂡a, 0)=[[2, 6]]. OK" +ECHO: "----- Lookup of 4-byte into mixed multi-byte" + WARNING: search term not found: "🂡" +ECHO: "Expect [] for search(🂡, aЛaЛaЛaЛa, 1)=[]. OK" +ECHO: "Expect [1] for search(🂡, a🂡a🂡a🂡a🂡a, 1)=[1]. OK" + WARNING: search term not found: "🂡" +ECHO: "Expect [[]] for search(🂡, aЛaЛaЛaЛa, 0)=[[]]. OK" +ECHO: "Expect [[1, 3, 5, 7]] for search(🂡, a🂡a🂡a🂡a🂡a, 0)=[[1, 3, 5, 7]]. OK" +ECHO: "Expect [[1, 3, 5, 7]] for search(🂡, a🂡Л🂡a🂡Л🂡a, 0)=[[1, 3, 5, 7]]. OK" +ECHO: "----- Lookup of mixed multi-byte into mixed multi-byte" + WARNING: search term not found: "🂡" +ECHO: "Expect [[0, 2, 4, 6, 8], [1, 3, 5, 7], []] for search(aЛ🂡, aЛaЛaЛaЛa, 0)=[[0, 2, 4, 6, 8], [1, 3, 5, 7], []]. OK" + WARNING: search term not found: "Л" +ECHO: "Expect [[0, 2, 4, 6, 8], [], [1, 3, 5, 7]] for search(aЛ🂡, a🂡a🂡a🂡a🂡a, 0)=[[0, 2, 4, 6, 8], [], [1, 3, 5, 7]]. OK" +ECHO: "Expect [[0, 4, 8], [2, 6], [1, 3, 5, 7]] for search(aЛ🂡, a🂡Л🂡a🂡Л🂡a, 0)=[[0, 4, 8], [2, 6], [1, 3, 5, 7]]. OK" +ECHO: "Expect [[1, 3, 5, 7], [0, 4, 8], [2, 6]] for search(🂡aЛ, a🂡Л🂡a🂡Л🂡a, 0)=[[1, 3, 5, 7], [0, 4, 8], [2, 6]]. OK" diff --git a/tests/regression/echotest/string-unicode-expected.echo b/tests/regression/echotest/string-unicode-expected.echo new file mode 100644 index 00000000..b4b848fd --- /dev/null +++ b/tests/regression/echotest/string-unicode-expected.echo @@ -0,0 +1,104 @@ +ECHO: "[", 0, "] = ", "DEADBEEF", " of len=", 8, ":" +ECHO: " [", 0, "]=", "D" +ECHO: " [", 1, "]=", "E" +ECHO: " [", 2, "]=", "A" +ECHO: " [", 3, "]=", "D" +ECHO: " [", 4, "]=", "B" +ECHO: " [", 5, "]=", "E" +ECHO: " [", 6, "]=", "E" +ECHO: " [", 7, "]=", "F" +ECHO: "[", 1, "] = ", "Ленивый рыжий кот", " of len=", 17, ":" +ECHO: " [", 0, "]=", "Л" +ECHO: " [", 1, "]=", "е" +ECHO: " [", 2, "]=", "н" +ECHO: " [", 3, "]=", "и" +ECHO: " [", 4, "]=", "в" +ECHO: " [", 5, "]=", "ы" +ECHO: " [", 6, "]=", "й" +ECHO: " [", 7, "]=", " " +ECHO: " [", 8, "]=", "р" +ECHO: " [", 9, "]=", "ы" +ECHO: " [", 10, "]=", "ж" +ECHO: " [", 11, "]=", "и" +ECHO: " [", 12, "]=", "й" +ECHO: " [", 13, "]=", " " +ECHO: " [", 14, "]=", "к" +ECHO: " [", 15, "]=", "о" +ECHO: " [", 16, "]=", "т" +ECHO: "[", 2, "] = ", "كسول الزنجبيل القط", " of len=", 18, ":" +ECHO: " [", 0, "]=", "ك" +ECHO: " [", 1, "]=", "س" +ECHO: " [", 2, "]=", "و" +ECHO: " [", 3, "]=", "ل" +ECHO: " [", 4, "]=", " " +ECHO: " [", 5, "]=", "ا" +ECHO: " [", 6, "]=", "ل" +ECHO: " [", 7, "]=", "ز" +ECHO: " [", 8, "]=", "ن" +ECHO: " [", 9, "]=", "ج" +ECHO: " [", 10, "]=", "ب" +ECHO: " [", 11, "]=", "ي" +ECHO: " [", 12, "]=", "ل" +ECHO: " [", 13, "]=", " " +ECHO: " [", 14, "]=", "ا" +ECHO: " [", 15, "]=", "ل" +ECHO: " [", 16, "]=", "ق" +ECHO: " [", 17, "]=", "ط" +ECHO: "[", 3, "] = ", "懶惰的姜貓", " of len=", 5, ":" +ECHO: " [", 0, "]=", "懶" +ECHO: " [", 1, "]=", "惰" +ECHO: " [", 2, "]=", "的" +ECHO: " [", 3, "]=", "姜" +ECHO: " [", 4, "]=", "貓" +ECHO: "[", 4, "] = ", "äöü ÄÖÜ ß", " of len=", 9, ":" +ECHO: " [", 0, "]=", "ä" +ECHO: " [", 1, "]=", "ö" +ECHO: " [", 2, "]=", "ü" +ECHO: " [", 3, "]=", " " +ECHO: " [", 4, "]=", "Ä" +ECHO: " [", 5, "]=", "Ö" +ECHO: " [", 6, "]=", "Ü" +ECHO: " [", 7, "]=", " " +ECHO: " [", 8, "]=", "ß" +ECHO: "[", 5, "] = ", "😁😂😃😄😅😆😇😈😉😊😋😌😍😎😏😐", " of len=", 16, ":" +ECHO: " [", 0, "]=", "😁" +ECHO: " [", 1, "]=", "😂" +ECHO: " [", 2, "]=", "😃" +ECHO: " [", 3, "]=", "😄" +ECHO: " [", 4, "]=", "😅" +ECHO: " [", 5, "]=", "😆" +ECHO: " [", 6, "]=", "😇" +ECHO: " [", 7, "]=", "😈" +ECHO: " [", 8, "]=", "😉" +ECHO: " [", 9, "]=", "😊" +ECHO: " [", 10, "]=", "😋" +ECHO: " [", 11, "]=", "😌" +ECHO: " [", 12, "]=", "😍" +ECHO: " [", 13, "]=", "😎" +ECHO: " [", 14, "]=", "😏" +ECHO: " [", 15, "]=", "😐" +ECHO: "[", 6, "] = ", "⠁⠂⠃⠄⠅⠆⠇⠈⠉⠊⠋⠌⠍⠎⠏", " of len=", 15, ":" +ECHO: " [", 0, "]=", "⠁" +ECHO: " [", 1, "]=", "⠂" +ECHO: " [", 2, "]=", "⠃" +ECHO: " [", 3, "]=", "⠄" +ECHO: " [", 4, "]=", "⠅" +ECHO: " [", 5, "]=", "⠆" +ECHO: " [", 6, "]=", "⠇" +ECHO: " [", 7, "]=", "⠈" +ECHO: " [", 8, "]=", "⠉" +ECHO: " [", 9, "]=", "⠊" +ECHO: " [", 10, "]=", "⠋" +ECHO: " [", 11, "]=", "⠌" +ECHO: " [", 12, "]=", "⠍" +ECHO: " [", 13, "]=", "⠎" +ECHO: " [", 14, "]=", "⠏" +ECHO: "[", 7, "] = ", "🂡🂱🃁🃑", " of len=", 4, ":" +ECHO: " [", 0, "]=", "🂡" +ECHO: " [", 1, "]=", "🂱" +ECHO: " [", 2, "]=", "🃁" +ECHO: " [", 3, "]=", "🃑" +ECHO: "Past end of unicode only 2-byte ", undef +ECHO: "Past end of unicode only 4-byte ", undef +ECHO: "Past end of both 2-byte ", undef +ECHO: "Past end of both 4-byte ", undef From 301ef946f05a320768d4b8f974a95e2a04a5fc0d Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Thu, 5 Dec 2013 12:20:40 -0500 Subject: [PATCH 22/66] Qt4 patches to make it build on 10.9 --- patches/qt4/patch-libtiff.diff | 18 + .../patch-src_corelib_global_qglobal.h.diff | 14 + ...ns_bearer_corewlan_qcorewlanengine.mm.diff | 1382 +++++++++++++++++ 3 files changed, 1414 insertions(+) create mode 100644 patches/qt4/patch-libtiff.diff create mode 100644 patches/qt4/patch-src_corelib_global_qglobal.h.diff create mode 100644 patches/qt4/patch-src_plugins_bearer_corewlan_qcorewlanengine.mm.diff diff --git a/patches/qt4/patch-libtiff.diff b/patches/qt4/patch-libtiff.diff new file mode 100644 index 00000000..5b7f9ec3 --- /dev/null +++ b/patches/qt4/patch-libtiff.diff @@ -0,0 +1,18 @@ +--- src/3rdparty/libtiff/libtiff/tif_config.h ++++ src/3rdparty/libtiff/libtiff/tif_config.h +@@ -317,15 +317,6 @@ + /* Define to empty if `const' does not conform to ANSI C. */ + /* #undef const */ + +-/* Define to `__inline__' or `__inline' if that's what the C compiler +- calls it, or to nothing if 'inline' is not supported under any name. */ +-#ifndef Q_OS_SYMBIAN +-#ifndef __cplusplus +-#undef inline +-#define inline +-#endif +-#endif +- + /* Define to `long int' if does not define. */ + /* #undef off_t */ + diff --git a/patches/qt4/patch-src_corelib_global_qglobal.h.diff b/patches/qt4/patch-src_corelib_global_qglobal.h.diff new file mode 100644 index 00000000..8c55c5a6 --- /dev/null +++ b/patches/qt4/patch-src_corelib_global_qglobal.h.diff @@ -0,0 +1,14 @@ +--- src/corelib/global/qglobal.h.orig 2013-06-07 07:16:52.000000000 +0200 ++++ src/corelib/global/qglobal.h 2013-10-27 14:05:22.000000000 +0100 +@@ -327,7 +327,10 @@ + # if !defined(MAC_OS_X_VERSION_10_8) + # define MAC_OS_X_VERSION_10_8 MAC_OS_X_VERSION_10_7 + 1 + # endif +-# if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_8) ++# if !defined(MAC_OS_X_VERSION_10_9) ++# define MAC_OS_X_VERSION_10_9 MAC_OS_X_VERSION_10_8 + 1 ++# endif ++# if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_9) + # warning "This version of Mac OS X is unsupported" + # endif + #endif diff --git a/patches/qt4/patch-src_plugins_bearer_corewlan_qcorewlanengine.mm.diff b/patches/qt4/patch-src_plugins_bearer_corewlan_qcorewlanengine.mm.diff new file mode 100644 index 00000000..61b2eef3 --- /dev/null +++ b/patches/qt4/patch-src_plugins_bearer_corewlan_qcorewlanengine.mm.diff @@ -0,0 +1,1382 @@ +--- src/plugins/bearer/corewlan/qcorewlanengine.mm ++++ src/plugins/bearer/corewlan/qcorewlanengine.mm +@@ -52,29 +52,17 @@ + #include + + #include +-#include +-#include +-#include +-#include +-#include +- +-#include +-#include +-#include +-#include +- +-#include ++ ++extern "C" { // Otherwise it won't find CWKeychain* symbols at link time ++#import ++} ++ + #include "private/qcore_mac_p.h" + + #include + #include + +-inline QString qt_NSStringToQString(const NSString *nsstr) +-{ return QCFString::toQString(reinterpret_cast(nsstr)); } +- +-inline NSString *qt_QStringToNSString(const QString &qstr) +-{ return [const_cast(reinterpret_cast(QCFString::toCFStringRef(qstr))) autorelease]; } +- ++#if __MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_7 + + @interface QT_MANGLE_NAMESPACE(QNSListener) : NSObject + { +@@ -86,6 +74,7 @@ inline NSString *qt_QStringToNSString(const QString &qstr) + - (void)notificationHandler;//:(NSNotification *)notification; + - (void)remove; + - (void)setEngine:(QCoreWlanEngine *)coreEngine; ++- (QCoreWlanEngine *)engine; + - (void)dealloc; + + @property (assign) QCoreWlanEngine* engine; +@@ -93,7 +82,6 @@ inline NSString *qt_QStringToNSString(const QString &qstr) + @end + + @implementation QT_MANGLE_NAMESPACE(QNSListener) +-@synthesize engine; + + - (id) init + { +@@ -101,7 +89,7 @@ inline NSString *qt_QStringToNSString(const QString &qstr) + NSAutoreleasePool *autoreleasepool = [[NSAutoreleasePool alloc] init]; + notificationCenter = [NSNotificationCenter defaultCenter]; + currentInterface = [CWInterface interfaceWithName:nil]; +- [notificationCenter addObserver:self selector:@selector(notificationHandler:) name:kCWPowerDidChangeNotification object:nil]; ++ [notificationCenter addObserver:self selector:@selector(notificationHandler:) name:CWPowerDidChangeNotification object:nil]; + [locker unlock]; + [autoreleasepool release]; + return self; +@@ -120,6 +108,11 @@ inline NSString *qt_QStringToNSString(const QString &qstr) + [locker unlock]; + } + ++-(QCoreWlanEngine *)engine ++{ ++ return engine; ++} ++ + -(void)remove + { + [locker lock]; +@@ -133,7 +126,7 @@ inline NSString *qt_QStringToNSString(const QString &qstr) + } + @end + +-QT_MANGLE_NAMESPACE(QNSListener) *listener = 0; ++static QT_MANGLE_NAMESPACE(QNSListener) *listener = 0; + + QT_BEGIN_NAMESPACE + +@@ -170,36 +163,28 @@ void QScanThread::run() + NSAutoreleasePool *autoreleasepool = [[NSAutoreleasePool alloc] init]; + QStringList found; + mutex.lock(); +- CWInterface *currentInterface = [CWInterface interfaceWithName:qt_QStringToNSString(interfaceName)]; ++ CWInterface *currentInterface = [CWInterface interfaceWithName: (NSString *)QCFString::toCFStringRef(interfaceName)]; + mutex.unlock(); + +- if([currentInterface power]) { ++ if (currentInterface.powerOn) { + NSError *err = nil; +- NSDictionary *parametersDict = [NSDictionary dictionaryWithObjectsAndKeys: +- [NSNumber numberWithBool:YES], kCWScanKeyMerge, +- [NSNumber numberWithInt:kCWScanTypeFast], kCWScanKeyScanType, +- [NSNumber numberWithInteger:100], kCWScanKeyRestTime, nil]; + +- NSArray* apArray = [currentInterface scanForNetworksWithParameters:parametersDict error:&err]; +- CWNetwork *apNetwork; ++ NSSet* apSet = [currentInterface scanForNetworksWithName:nil error:&err]; + + if (!err) { +- +- for(uint row=0; row < [apArray count]; row++ ) { +- apNetwork = [apArray objectAtIndex:row]; +- +- const QString networkSsid = qt_NSStringToQString([apNetwork ssid]); ++ for (CWNetwork *apNetwork in apSet) { ++ const QString networkSsid = QCFString::toQString(CFStringRef([apNetwork ssid])); + const QString id = QString::number(qHash(QLatin1String("corewlan:") + networkSsid)); + found.append(id); + + QNetworkConfiguration::StateFlags state = QNetworkConfiguration::Undefined; + bool known = isKnownSsid(networkSsid); +- if( [currentInterface.interfaceState intValue] == kCWInterfaceStateRunning) { +- if( networkSsid == qt_NSStringToQString( [currentInterface ssid])) { ++ if (currentInterface.serviceActive) { ++ if( networkSsid == QCFString::toQString(CFStringRef([currentInterface ssid]))) { + state = QNetworkConfiguration::Active; + } + } +- if(state == QNetworkConfiguration::Undefined) { ++ if (state == QNetworkConfiguration::Undefined) { + if(known) { + state = QNetworkConfiguration::Discovered; + } else { +@@ -207,7 +192,7 @@ void QScanThread::run() + } + } + QNetworkConfiguration::Purpose purpose = QNetworkConfiguration::UnknownPurpose; +- if([[apNetwork securityMode] intValue] == kCWSecurityModeOpen) { ++ if ([apNetwork supportsSecurity:kCWSecurityNone]) { + purpose = QNetworkConfiguration::PublicPurpose; + } else { + purpose = QNetworkConfiguration::PrivatePurpose; +@@ -237,8 +222,8 @@ void QScanThread::run() + interfaceName = ij.value(); + } + +- if( [currentInterface.interfaceState intValue] == kCWInterfaceStateRunning) { +- if( networkSsid == qt_NSStringToQString([currentInterface ssid])) { ++ if (currentInterface.serviceActive) { ++ if( networkSsid == QCFString::toQString(CFStringRef([currentInterface ssid]))) { + state = QNetworkConfiguration::Active; + } + } +@@ -300,14 +285,14 @@ void QScanThread::getUserConfigurations() + NSAutoreleasePool *autoreleasepool = [[NSAutoreleasePool alloc] init]; + userProfiles.clear(); + +- NSArray *wifiInterfaces = [CWInterface supportedInterfaces]; +- for(uint row=0; row < [wifiInterfaces count]; row++ ) { ++ NSSet *wifiInterfaces = [CWInterface interfaceNames]; ++ for (NSString *ifName in wifiInterfaces) { + +- CWInterface *wifiInterface = [CWInterface interfaceWithName: [wifiInterfaces objectAtIndex:row]]; +- if ( ![wifiInterface power] ) ++ CWInterface *wifiInterface = [CWInterface interfaceWithName: ifName]; ++ if (!wifiInterface.powerOn) + continue; + +- NSString *nsInterfaceName = [wifiInterface name]; ++ NSString *nsInterfaceName = wifiInterface.ssid; + // add user configured system networks + SCDynamicStoreRef dynRef = SCDynamicStoreCreate(kCFAllocatorSystemDefault, (CFStringRef)@"Qt corewlan", nil, nil); + NSDictionary * airportPlist = (NSDictionary *)SCDynamicStoreCopyValue(dynRef, (CFStringRef)[NSString stringWithFormat:@"Setup:/Network/Interface/%@/AirPort", nsInterfaceName]); +@@ -316,11 +301,11 @@ void QScanThread::getUserConfigurations() + NSDictionary *prefNetDict = [airportPlist objectForKey:@"PreferredNetworks"]; + + NSArray *thisSsidarray = [prefNetDict valueForKey:@"SSID_STR"]; +- for(NSString *ssidkey in thisSsidarray) { +- QString thisSsid = qt_NSStringToQString(ssidkey); ++ for (NSString *ssidkey in thisSsidarray) { ++ QString thisSsid = QCFString::toQString(CFStringRef(ssidkey)); + if(!userProfiles.contains(thisSsid)) { + QMap map; +- map.insert(thisSsid, qt_NSStringToQString(nsInterfaceName)); ++ map.insert(thisSsid, QCFString::toQString(CFStringRef(nsInterfaceName))); + userProfiles.insert(thisSsid, map); + } + } +@@ -329,7 +314,7 @@ void QScanThread::getUserConfigurations() + + // 802.1X user profiles + QString userProfilePath = QDir::homePath() + "/Library/Preferences/com.apple.eap.profiles.plist"; +- NSDictionary* eapDict = [[[NSDictionary alloc] initWithContentsOfFile:qt_QStringToNSString(userProfilePath)] autorelease]; ++ NSDictionary* eapDict = [[[NSDictionary alloc] initWithContentsOfFile: (NSString *)QCFString::toCFStringRef(userProfilePath)] autorelease]; + if(eapDict != nil) { + NSString *profileStr= @"Profiles"; + NSString *nameStr = @"UserDefinedName"; +@@ -348,15 +333,15 @@ void QScanThread::getUserConfigurations() + QString ssid; + for(int i = 0; i < dictSize; i++) { + if([nameStr isEqualToString:keys[i]]) { +- networkName = qt_NSStringToQString(objects[i]); ++ networkName = QCFString::toQString(CFStringRef(objects[i])); + } + if([networkSsidStr isEqualToString:keys[i]]) { +- ssid = qt_NSStringToQString(objects[i]); ++ ssid = QCFString::toQString(CFStringRef(objects[i])); + } + if(!userProfiles.contains(networkName) + && !ssid.isEmpty()) { + QMap map; +- map.insert(ssid, qt_NSStringToQString(nsInterfaceName)); ++ map.insert(ssid, QCFString::toQString(CFStringRef(nsInterfaceName))); + userProfiles.insert(networkName, map); + } + } +@@ -444,7 +429,7 @@ void QCoreWlanEngine::initialize() + QMutexLocker locker(&mutex); + NSAutoreleasePool *autoreleasepool = [[NSAutoreleasePool alloc] init]; + +- if([[CWInterface supportedInterfaces] count] > 0 && !listener) { ++ if ([[CWInterface interfaceNames] count] > 0 && !listener) { + listener = [[QT_MANGLE_NAMESPACE(QNSListener) alloc] init]; + listener.engine = this; + hasWifi = true; +@@ -479,141 +464,68 @@ void QCoreWlanEngine::connectToId(const QString &id) + QString interfaceString = getInterfaceFromId(id); + + CWInterface *wifiInterface = +- [CWInterface interfaceWithName: qt_QStringToNSString(interfaceString)]; ++ [CWInterface interfaceWithName: (NSString *)QCFString::toCFStringRef(interfaceString)]; + +- if ([wifiInterface power]) { ++ if (wifiInterface.powerOn) { + NSError *err = nil; +- NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0]; +- + QString wantedSsid; +- + QNetworkConfigurationPrivatePointer ptr = accessPointConfigurations.value(id); + + const QString idHash = QString::number(qHash(QLatin1String("corewlan:") + ptr->name)); + const QString idHash2 = QString::number(qHash(QLatin1String("corewlan:") + scanThread->getNetworkNameFromSsid(ptr->name))); + +- bool using8021X = false; +- if (idHash2 != id) { +- NSArray *array = [CW8021XProfile allUser8021XProfiles]; +- +- for (NSUInteger i = 0; i < [array count]; ++i) { +- const QString networkNameHashCheck = QString::number(qHash(QLatin1String("corewlan:") + qt_NSStringToQString([[array objectAtIndex:i] userDefinedName]))); +- +- const QString ssidHash = QString::number(qHash(QLatin1String("corewlan:") + qt_NSStringToQString([[array objectAtIndex:i] ssid]))); +- +- if (id == networkNameHashCheck || id == ssidHash) { +- const QString thisName = scanThread->getSsidFromNetworkName(id); +- if (thisName.isEmpty()) +- wantedSsid = id; +- else +- wantedSsid = thisName; +- +- [params setValue: [array objectAtIndex:i] forKey:kCWAssocKey8021XProfile]; +- using8021X = true; +- break; +- } ++ QString wantedNetwork; ++ QMapIterator > i(scanThread->userProfiles); ++ while (i.hasNext()) { ++ i.next(); ++ wantedNetwork = i.key(); ++ const QString networkNameHash = QString::number(qHash(QLatin1String("corewlan:") + wantedNetwork)); ++ if (id == networkNameHash) { ++ wantedSsid = scanThread->getSsidFromNetworkName(wantedNetwork); ++ break; + } + } + +- if (!using8021X) { +- QString wantedNetwork; +- QMapIterator > i(scanThread->userProfiles); +- while (i.hasNext()) { +- i.next(); +- wantedNetwork = i.key(); +- const QString networkNameHash = QString::number(qHash(QLatin1String("corewlan:") + wantedNetwork)); +- if (id == networkNameHash) { +- wantedSsid = scanThread->getSsidFromNetworkName(wantedNetwork); +- break; +- } +- } +- } +- NSDictionary *scanParameters = [NSDictionary dictionaryWithObjectsAndKeys: +- [NSNumber numberWithBool:YES], kCWScanKeyMerge, +- [NSNumber numberWithInt:kCWScanTypeFast], kCWScanKeyScanType, +- [NSNumber numberWithInteger:100], kCWScanKeyRestTime, +- qt_QStringToNSString(wantedSsid), kCWScanKeySSID, +- nil]; +- +- NSArray *scanArray = [wifiInterface scanForNetworksWithParameters:scanParameters error:&err]; ++ NSSet *scanSet = [wifiInterface scanForNetworksWithName:(NSString *)QCFString::toCFStringRef(wantedSsid) error:&err]; + + if(!err) { +- for(uint row=0; row < [scanArray count]; row++ ) { +- CWNetwork *apNetwork = [scanArray objectAtIndex:row]; +- +- if(wantedSsid == qt_NSStringToQString([apNetwork ssid])) { +- +- if(!using8021X) { +- SecKeychainAttribute attributes[3]; +- +- NSString *account = [apNetwork ssid]; +- NSString *keyKind = @"AirPort network password"; +- NSString *keyName = account; +- +- attributes[0].tag = kSecAccountItemAttr; +- attributes[0].data = (void *)[account UTF8String]; +- attributes[0].length = [account length]; +- +- attributes[1].tag = kSecDescriptionItemAttr; +- attributes[1].data = (void *)[keyKind UTF8String]; +- attributes[1].length = [keyKind length]; +- +- attributes[2].tag = kSecLabelItemAttr; +- attributes[2].data = (void *)[keyName UTF8String]; +- attributes[2].length = [keyName length]; +- +- SecKeychainAttributeList attributeList = {3,attributes}; +- +- SecKeychainSearchRef searchRef; +- SecKeychainSearchCreateFromAttributes(NULL, kSecGenericPasswordItemClass, &attributeList, &searchRef); +- +- NSString *password = @""; +- SecKeychainItemRef searchItem; +- +- if (SecKeychainSearchCopyNext(searchRef, &searchItem) == noErr) { +- UInt32 realPasswordLength; +- SecKeychainAttribute attributesW[8]; +- attributesW[0].tag = kSecAccountItemAttr; +- SecKeychainAttributeList listW = {1,attributesW}; +- char *realPassword; +- OSStatus status = SecKeychainItemCopyContent(searchItem, NULL, &listW, &realPasswordLength,(void **)&realPassword); +- +- if (status == noErr) { +- if (realPassword != NULL) { +- +- QByteArray pBuf; +- pBuf.resize(realPasswordLength); +- pBuf.prepend(realPassword); +- pBuf.insert(realPasswordLength,'\0'); +- +- password = [NSString stringWithUTF8String:pBuf]; +- } +- SecKeychainItemFreeContent(&listW, realPassword); +- } +- +- CFRelease(searchItem); +- } else { +- qDebug() << "SecKeychainSearchCopyNext error"; +- } +- [params setValue: password forKey: kCWAssocKeyPassphrase]; +- } // end using8021X +- +- +- bool result = [wifiInterface associateToNetwork: apNetwork parameters:[NSDictionary dictionaryWithDictionary:params] error:&err]; ++ for (CWNetwork *apNetwork in scanSet) { ++ CFDataRef ssidData = (CFDataRef)[apNetwork ssidData]; ++ bool result = false; ++ ++ SecIdentityRef identity = 0; ++ // Check first whether we require IEEE 802.1X authentication for the wanted SSID ++ if (CWKeychainCopyEAPIdentity(ssidData, &identity) == errSecSuccess) { ++ CFStringRef username = 0; ++ CFStringRef password = 0; ++ if (CWKeychainCopyEAPUsernameAndPassword(ssidData, &username, &password) == errSecSuccess) { ++ result = [wifiInterface associateToEnterpriseNetwork:apNetwork ++ identity:identity username:(NSString *)username password:(NSString *)password ++ error:&err]; ++ CFRelease(username); ++ CFRelease(password); ++ } ++ CFRelease(identity); ++ } else { ++ CFStringRef password = 0; ++ if (CWKeychainCopyPassword(ssidData, &password) == errSecSuccess) { ++ result = [wifiInterface associateToNetwork:apNetwork password:(NSString *)password error:&err]; ++ CFRelease(password); ++ } ++ } + +- if(!err) { +- if(!result) { +- emit connectionError(id, ConnectError); +- } else { +- return; +- } ++ if (!err) { ++ if (!result) { ++ emit connectionError(id, ConnectError); + } else { +- qDebug() <<"associate ERROR"<< qt_NSStringToQString([err localizedDescription ]); ++ return; + } ++ } else { ++ qDebug() <<"associate ERROR"<< QCFString::toQString(CFStringRef([err localizedDescription ])); + } + } //end scan network + } else { +- qDebug() <<"scan ERROR"<< qt_NSStringToQString([err localizedDescription ]); ++ qDebug() <<"scan ERROR"<< QCFString::toQString(CFStringRef([err localizedDescription ])); + } + emit connectionError(id, InterfaceLookupError); + } +@@ -631,10 +543,10 @@ void QCoreWlanEngine::disconnectFromId(const QString &id) + NSAutoreleasePool *autoreleasepool = [[NSAutoreleasePool alloc] init]; + + CWInterface *wifiInterface = +- [CWInterface interfaceWithName: qt_QStringToNSString(interfaceString)]; ++ [CWInterface interfaceWithName: (NSString *)QCFString::toCFStringRef(interfaceString)]; + + [wifiInterface disassociate]; +- if ([[wifiInterface interfaceState]intValue] != kCWInterfaceStateInactive) { ++ if (wifiInterface.serviceActive) { + locker.unlock(); + emit connectionError(id, DisconnectionError); + locker.relock(); +@@ -654,9 +566,9 @@ void QCoreWlanEngine::doRequestUpdate() + + NSAutoreleasePool *autoreleasepool = [[NSAutoreleasePool alloc] init]; + +- NSArray *wifiInterfaces = [CWInterface supportedInterfaces]; +- for (uint row = 0; row < [wifiInterfaces count]; ++row) { +- scanThread->interfaceName = qt_NSStringToQString([wifiInterfaces objectAtIndex:row]); ++ NSSet *wifiInterfaces = [CWInterface interfaceNames]; ++ for (NSString *ifName in wifiInterfaces) { ++ scanThread->interfaceName = QCFString::toQString(CFStringRef(ifName)); + scanThread->start(); + } + locker.unlock(); +@@ -669,8 +581,8 @@ bool QCoreWlanEngine::isWifiReady(const QString &wifiDeviceName) + bool haswifi = false; + if(hasWifi) { + NSAutoreleasePool *autoreleasepool = [[NSAutoreleasePool alloc] init]; +- CWInterface *defaultInterface = [CWInterface interfaceWithName: qt_QStringToNSString(wifiDeviceName)]; +- if([defaultInterface power]) { ++ CWInterface *defaultInterface = [CWInterface interfaceWithName: (NSString *)QCFString::toCFStringRef(wifiDeviceName)]; ++ if (defaultInterface.powerOn) { + haswifi = true; + } + [autoreleasepool release]; +@@ -898,7 +810,7 @@ quint64 QCoreWlanEngine::startTime(const QString &identifier) + bool ok = false; + for(int i = 0; i < dictSize; i++) { + if([ssidStr isEqualToString:keys[i]]) { +- const QString ident = QString::number(qHash(QLatin1String("corewlan:") + qt_NSStringToQString(objects[i]))); ++ const QString ident = QString::number(qHash(QLatin1String("corewlan:") + QCFString::toQString(CFStringRef(objects[i])))); + if(ident == identifier) { + ok = true; + } +@@ -944,3 +856,7 @@ quint64 QCoreWlanEngine::getBytes(const QString &interfaceName, bool b) + } + + QT_END_NAMESPACE ++ ++#else // QT_MAC_PLATFORM_SDK_EQUAL_OR_ABOVE ++#include "qcorewlanengine_10_6.mm" ++#endif +diff --git a/src/plugins/bearer/corewlan/qcorewlanengine_10_6.mm b/src/plugins/bearer/corewlan/qcorewlanengine_10_6.mm +new file mode 100644 +index 0000000..a3bf615 +--- /dev/null ++++ src/plugins/bearer/corewlan/qcorewlanengine_10_6.mm +@@ -0,0 +1,916 @@ ++/**************************************************************************** ++** ++** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). ++** Contact: http://www.qt-project.org/legal ++** ++** This file is part of the plugins of the Qt Toolkit. ++** ++** $QT_BEGIN_LICENSE:LGPL$ ++** Commercial License Usage ++** Licensees holding valid commercial Qt licenses may use this file in ++** accordance with the commercial license agreement provided with the ++** Software or, alternatively, in accordance with the terms contained in ++** a written agreement between you and Digia. For licensing terms and ++** conditions see http://qt.digia.com/licensing. For further information ++** use the contact form at http://qt.digia.com/contact-us. ++** ++** GNU Lesser General Public License Usage ++** Alternatively, this file may be used under the terms of the GNU Lesser ++** General Public License version 2.1 as published by the Free Software ++** Foundation and appearing in the file LICENSE.LGPL included in the ++** packaging of this file. Please review the following information to ++** ensure the GNU Lesser General Public License version 2.1 requirements ++** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ++** ++** In addition, as a special exception, Digia gives you certain additional ++** rights. These rights are described in the Digia Qt LGPL Exception ++** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ++** ++** GNU General Public License Usage ++** Alternatively, this file may be used under the terms of the GNU ++** General Public License version 3.0 as published by the Free Software ++** Foundation and appearing in the file LICENSE.GPL included in the ++** packaging of this file. Please review the following information to ++** ensure the GNU General Public License version 3.0 requirements will be ++** met: http://www.gnu.org/copyleft/gpl.html. ++** ++** ++** $QT_END_LICENSE$ ++** ++****************************************************************************/ ++ ++#include ++ ++@interface QT_MANGLE_NAMESPACE(QNSListener) : NSObject ++{ ++ NSNotificationCenter *notificationCenter; ++ CWInterface *currentInterface; ++ QCoreWlanEngine *engine; ++ NSLock *locker; ++} ++- (void)notificationHandler;//:(NSNotification *)notification; ++- (void)remove; ++- (void)setEngine:(QCoreWlanEngine *)coreEngine; ++- (QCoreWlanEngine *)engine; ++- (void)dealloc; ++ ++@property (assign) QCoreWlanEngine* engine; ++ ++@end ++ ++@implementation QT_MANGLE_NAMESPACE(QNSListener) ++ ++- (id) init ++{ ++ [locker lock]; ++ NSAutoreleasePool *autoreleasepool = [[NSAutoreleasePool alloc] init]; ++ notificationCenter = [NSNotificationCenter defaultCenter]; ++ currentInterface = [CWInterface interfaceWithName:nil]; ++ [notificationCenter addObserver:self selector:@selector(notificationHandler:) name:kCWPowerDidChangeNotification object:nil]; ++ [locker unlock]; ++ [autoreleasepool release]; ++ return self; ++} ++ ++-(void)dealloc ++{ ++ [super dealloc]; ++} ++ ++-(void)setEngine:(QCoreWlanEngine *)coreEngine ++{ ++ [locker lock]; ++ if(!engine) ++ engine = coreEngine; ++ [locker unlock]; ++} ++ ++-(QCoreWlanEngine *)engine ++{ ++ return engine; ++} ++ ++-(void)remove ++{ ++ [locker lock]; ++ [notificationCenter removeObserver:self]; ++ [locker unlock]; ++} ++ ++- (void)notificationHandler//:(NSNotification *)notification ++{ ++ engine->requestUpdate(); ++} ++@end ++ ++static QT_MANGLE_NAMESPACE(QNSListener) *listener = 0; ++ ++QT_BEGIN_NAMESPACE ++ ++void networkChangeCallback(SCDynamicStoreRef/* store*/, CFArrayRef changedKeys, void *info) ++{ ++ for ( long i = 0; i < CFArrayGetCount(changedKeys); i++) { ++ ++ QString changed = QCFString::toQString(CFStringRef((CFStringRef)CFArrayGetValueAtIndex(changedKeys, i))); ++ if( changed.contains("/Network/Global/IPv4")) { ++ QCoreWlanEngine* wlanEngine = static_cast(info); ++ wlanEngine->requestUpdate(); ++ } ++ } ++ return; ++} ++ ++ ++QScanThread::QScanThread(QObject *parent) ++ :QThread(parent) ++{ ++} ++ ++QScanThread::~QScanThread() ++{ ++} ++ ++void QScanThread::quit() ++{ ++ wait(); ++} ++ ++void QScanThread::run() ++{ ++ NSAutoreleasePool *autoreleasepool = [[NSAutoreleasePool alloc] init]; ++ QStringList found; ++ mutex.lock(); ++ CWInterface *currentInterface = [CWInterface interfaceWithName: (NSString *)QCFString::toCFStringRef(interfaceName)]; ++ mutex.unlock(); ++ ++ if([currentInterface power]) { ++ NSError *err = nil; ++ NSDictionary *parametersDict = [NSDictionary dictionaryWithObjectsAndKeys: ++ [NSNumber numberWithBool:YES], kCWScanKeyMerge, ++ [NSNumber numberWithInt:kCWScanTypeFast], kCWScanKeyScanType, ++ [NSNumber numberWithInteger:100], kCWScanKeyRestTime, nil]; ++ ++ NSArray* apArray = [currentInterface scanForNetworksWithParameters:parametersDict error:&err]; ++ CWNetwork *apNetwork; ++ ++ if (!err) { ++ ++ for(uint row=0; row < [apArray count]; row++ ) { ++ apNetwork = [apArray objectAtIndex:row]; ++ ++ const QString networkSsid = QCFString::toQString(CFStringRef([apNetwork ssid])); ++ const QString id = QString::number(qHash(QLatin1String("corewlan:") + networkSsid)); ++ found.append(id); ++ ++ QNetworkConfiguration::StateFlags state = QNetworkConfiguration::Undefined; ++ bool known = isKnownSsid(networkSsid); ++ if( [currentInterface.interfaceState intValue] == kCWInterfaceStateRunning) { ++ if( networkSsid == QCFString::toQString(CFStringRef([currentInterface ssid]))) { ++ state = QNetworkConfiguration::Active; ++ } ++ } ++ if(state == QNetworkConfiguration::Undefined) { ++ if(known) { ++ state = QNetworkConfiguration::Discovered; ++ } else { ++ state = QNetworkConfiguration::Undefined; ++ } ++ } ++ QNetworkConfiguration::Purpose purpose = QNetworkConfiguration::UnknownPurpose; ++ if([[apNetwork securityMode] intValue] == kCWSecurityModeOpen) { ++ purpose = QNetworkConfiguration::PublicPurpose; ++ } else { ++ purpose = QNetworkConfiguration::PrivatePurpose; ++ } ++ ++ found.append(foundNetwork(id, networkSsid, state, interfaceName, purpose)); ++ ++ } ++ } ++ } ++ // add known configurations that are not around. ++ QMapIterator > i(userProfiles); ++ while (i.hasNext()) { ++ i.next(); ++ ++ QString networkName = i.key(); ++ const QString id = QString::number(qHash(QLatin1String("corewlan:") + networkName)); ++ ++ if(!found.contains(id)) { ++ QString networkSsid = getSsidFromNetworkName(networkName); ++ const QString ssidId = QString::number(qHash(QLatin1String("corewlan:") + networkSsid)); ++ QNetworkConfiguration::StateFlags state = QNetworkConfiguration::Undefined; ++ QString interfaceName; ++ QMapIterator ij(i.value()); ++ while (ij.hasNext()) { ++ ij.next(); ++ interfaceName = ij.value(); ++ } ++ ++ if( [currentInterface.interfaceState intValue] == kCWInterfaceStateRunning) { ++ if( networkSsid == QCFString::toQString(CFStringRef([currentInterface ssid]))) { ++ state = QNetworkConfiguration::Active; ++ } ++ } ++ if(state == QNetworkConfiguration::Undefined) { ++ if( userProfiles.contains(networkName) ++ && found.contains(ssidId)) { ++ state = QNetworkConfiguration::Discovered; ++ } ++ } ++ ++ if(state == QNetworkConfiguration::Undefined) { ++ state = QNetworkConfiguration::Defined; ++ } ++ ++ found.append(foundNetwork(id, networkName, state, interfaceName, QNetworkConfiguration::UnknownPurpose)); ++ } ++ } ++ emit networksChanged(); ++ [autoreleasepool release]; ++} ++ ++QStringList QScanThread::foundNetwork(const QString &id, const QString &name, const QNetworkConfiguration::StateFlags state, const QString &interfaceName, const QNetworkConfiguration::Purpose purpose) ++{ ++ QStringList found; ++ QMutexLocker locker(&mutex); ++ QNetworkConfigurationPrivate *ptr = new QNetworkConfigurationPrivate; ++ ++ ptr->name = name; ++ ptr->isValid = true; ++ ptr->id = id; ++ ptr->state = state; ++ ptr->type = QNetworkConfiguration::InternetAccessPoint; ++ ptr->bearerType = QNetworkConfiguration::BearerWLAN; ++ ptr->purpose = purpose; ++ ++ fetchedConfigurations.append( ptr); ++ configurationInterface.insert(ptr->id, interfaceName); ++ ++ locker.unlock(); ++ locker.relock(); ++ found.append(id); ++ return found; ++} ++ ++QList QScanThread::getConfigurations() ++{ ++ QMutexLocker locker(&mutex); ++ ++ QList foundConfigurations = fetchedConfigurations; ++ fetchedConfigurations.clear(); ++ ++ return foundConfigurations; ++} ++ ++void QScanThread::getUserConfigurations() ++{ ++ QMutexLocker locker(&mutex); ++ ++ NSAutoreleasePool *autoreleasepool = [[NSAutoreleasePool alloc] init]; ++ userProfiles.clear(); ++ ++ NSArray *wifiInterfaces = [CWInterface supportedInterfaces]; ++ for(uint row=0; row < [wifiInterfaces count]; row++ ) { ++ ++ CWInterface *wifiInterface = [CWInterface interfaceWithName: [wifiInterfaces objectAtIndex:row]]; ++ if ( ![wifiInterface power] ) ++ continue; ++ ++ NSString *nsInterfaceName = [wifiInterface name]; ++// add user configured system networks ++ SCDynamicStoreRef dynRef = SCDynamicStoreCreate(kCFAllocatorSystemDefault, (CFStringRef)@"Qt corewlan", nil, nil); ++ NSDictionary * airportPlist = (NSDictionary *)SCDynamicStoreCopyValue(dynRef, (CFStringRef)[NSString stringWithFormat:@"Setup:/Network/Interface/%@/AirPort", nsInterfaceName]); ++ CFRelease(dynRef); ++ if(airportPlist != nil) { ++ NSDictionary *prefNetDict = [airportPlist objectForKey:@"PreferredNetworks"]; ++ ++ NSArray *thisSsidarray = [prefNetDict valueForKey:@"SSID_STR"]; ++ for(NSString *ssidkey in thisSsidarray) { ++ QString thisSsid = QCFString::toQString(CFStringRef(ssidkey)); ++ if(!userProfiles.contains(thisSsid)) { ++ QMap map; ++ map.insert(thisSsid, QCFString::toQString(CFStringRef(nsInterfaceName))); ++ userProfiles.insert(thisSsid, map); ++ } ++ } ++ CFRelease(airportPlist); ++ } ++ ++ // 802.1X user profiles ++ QString userProfilePath = QDir::homePath() + "/Library/Preferences/com.apple.eap.profiles.plist"; ++ NSDictionary* eapDict = [[[NSDictionary alloc] initWithContentsOfFile: (NSString *)QCFString::toCFStringRef(userProfilePath)] autorelease]; ++ if(eapDict != nil) { ++ NSString *profileStr= @"Profiles"; ++ NSString *nameStr = @"UserDefinedName"; ++ NSString *networkSsidStr = @"Wireless Network"; ++ for (id profileKey in eapDict) { ++ if ([profileStr isEqualToString:profileKey]) { ++ NSDictionary *itemDict = [eapDict objectForKey:profileKey]; ++ for (id itemKey in itemDict) { ++ ++ NSInteger dictSize = [itemKey count]; ++ id objects[dictSize]; ++ id keys[dictSize]; ++ ++ [itemKey getObjects:objects andKeys:keys]; ++ QString networkName; ++ QString ssid; ++ for(int i = 0; i < dictSize; i++) { ++ if([nameStr isEqualToString:keys[i]]) { ++ networkName = QCFString::toQString(CFStringRef(objects[i])); ++ } ++ if([networkSsidStr isEqualToString:keys[i]]) { ++ ssid = QCFString::toQString(CFStringRef(objects[i])); ++ } ++ if(!userProfiles.contains(networkName) ++ && !ssid.isEmpty()) { ++ QMap map; ++ map.insert(ssid, QCFString::toQString(CFStringRef(nsInterfaceName))); ++ userProfiles.insert(networkName, map); ++ } ++ } ++ } ++ } ++ } ++ } ++ } ++ [autoreleasepool release]; ++} ++ ++QString QScanThread::getSsidFromNetworkName(const QString &name) ++{ ++ QMutexLocker locker(&mutex); ++ ++ QMapIterator > i(userProfiles); ++ while (i.hasNext()) { ++ i.next(); ++ QMap map = i.value(); ++ QMapIterator ij(i.value()); ++ while (ij.hasNext()) { ++ ij.next(); ++ const QString networkNameHash = QString::number(qHash(QLatin1String("corewlan:") +i.key())); ++ if(name == i.key() || name == networkNameHash) { ++ return ij.key(); ++ } ++ } ++ } ++ return QString(); ++} ++ ++QString QScanThread::getNetworkNameFromSsid(const QString &ssid) ++{ ++ QMutexLocker locker(&mutex); ++ ++ QMapIterator > i(userProfiles); ++ while (i.hasNext()) { ++ i.next(); ++ QMap map = i.value(); ++ QMapIterator ij(i.value()); ++ while (ij.hasNext()) { ++ ij.next(); ++ if(ij.key() == ssid) { ++ return i.key(); ++ } ++ } ++ } ++ return QString(); ++} ++ ++bool QScanThread::isKnownSsid(const QString &ssid) ++{ ++ QMutexLocker locker(&mutex); ++ ++ QMapIterator > i(userProfiles); ++ while (i.hasNext()) { ++ i.next(); ++ QMap map = i.value(); ++ if(map.keys().contains(ssid)) { ++ return true; ++ } ++ } ++ return false; ++} ++ ++ ++QCoreWlanEngine::QCoreWlanEngine(QObject *parent) ++: QBearerEngineImpl(parent), scanThread(0) ++{ ++ scanThread = new QScanThread(this); ++ connect(scanThread, SIGNAL(networksChanged()), ++ this, SLOT(networksChanged())); ++} ++ ++QCoreWlanEngine::~QCoreWlanEngine() ++{ ++ while (!foundConfigurations.isEmpty()) ++ delete foundConfigurations.takeFirst(); ++ [listener remove]; ++ [listener release]; ++} ++ ++void QCoreWlanEngine::initialize() ++{ ++ QMutexLocker locker(&mutex); ++ NSAutoreleasePool *autoreleasepool = [[NSAutoreleasePool alloc] init]; ++ ++ if([[CWInterface supportedInterfaces] count] > 0 && !listener) { ++ listener = [[QT_MANGLE_NAMESPACE(QNSListener) alloc] init]; ++ listener.engine = this; ++ hasWifi = true; ++ } else { ++ hasWifi = false; ++ } ++ storeSession = NULL; ++ ++ startNetworkChangeLoop(); ++ [autoreleasepool release]; ++} ++ ++ ++QString QCoreWlanEngine::getInterfaceFromId(const QString &id) ++{ ++ QMutexLocker locker(&mutex); ++ ++ return scanThread->configurationInterface.value(id); ++} ++ ++bool QCoreWlanEngine::hasIdentifier(const QString &id) ++{ ++ QMutexLocker locker(&mutex); ++ ++ return scanThread->configurationInterface.contains(id); ++} ++ ++void QCoreWlanEngine::connectToId(const QString &id) ++{ ++ QMutexLocker locker(&mutex); ++ NSAutoreleasePool *autoreleasepool = [[NSAutoreleasePool alloc] init]; ++ QString interfaceString = getInterfaceFromId(id); ++ ++ CWInterface *wifiInterface = ++ [CWInterface interfaceWithName: (NSString *)QCFString::toCFStringRef(interfaceString)]; ++ ++ if ([wifiInterface power]) { ++ NSError *err = nil; ++ NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0]; ++ ++ QString wantedSsid; ++ ++ QNetworkConfigurationPrivatePointer ptr = accessPointConfigurations.value(id); ++ ++ const QString idHash = QString::number(qHash(QLatin1String("corewlan:") + ptr->name)); ++ const QString idHash2 = QString::number(qHash(QLatin1String("corewlan:") + scanThread->getNetworkNameFromSsid(ptr->name))); ++ ++ bool using8021X = false; ++ if (idHash2 != id) { ++ NSArray *array = [CW8021XProfile allUser8021XProfiles]; ++ ++ for (NSUInteger i = 0; i < [array count]; ++i) { ++ const QString networkNameHashCheck = QString::number(qHash(QLatin1String("corewlan:") + QCFString::toQString(CFStringRef([[array objectAtIndex:i] userDefinedName])))); ++ ++ const QString ssidHash = QString::number(qHash(QLatin1String("corewlan:") + QCFString::toQString(CFStringRef([[array objectAtIndex:i] ssid])))); ++ ++ if (id == networkNameHashCheck || id == ssidHash) { ++ const QString thisName = scanThread->getSsidFromNetworkName(id); ++ if (thisName.isEmpty()) ++ wantedSsid = id; ++ else ++ wantedSsid = thisName; ++ ++ [params setValue: [array objectAtIndex:i] forKey:kCWAssocKey8021XProfile]; ++ using8021X = true; ++ break; ++ } ++ } ++ } ++ ++ if (!using8021X) { ++ QString wantedNetwork; ++ QMapIterator > i(scanThread->userProfiles); ++ while (i.hasNext()) { ++ i.next(); ++ wantedNetwork = i.key(); ++ const QString networkNameHash = QString::number(qHash(QLatin1String("corewlan:") + wantedNetwork)); ++ if (id == networkNameHash) { ++ wantedSsid = scanThread->getSsidFromNetworkName(wantedNetwork); ++ break; ++ } ++ } ++ } ++ NSDictionary *scanParameters = [NSDictionary dictionaryWithObjectsAndKeys: ++ [NSNumber numberWithBool:YES], kCWScanKeyMerge, ++ [NSNumber numberWithInt:kCWScanTypeFast], kCWScanKeyScanType, ++ [NSNumber numberWithInteger:100], kCWScanKeyRestTime, ++ (NSString *)QCFString::toCFStringRef(wantedSsid), kCWScanKeySSID, ++ nil]; ++ ++ NSArray *scanArray = [wifiInterface scanForNetworksWithParameters:scanParameters error:&err]; ++ ++ if(!err) { ++ for(uint row=0; row < [scanArray count]; row++ ) { ++ CWNetwork *apNetwork = [scanArray objectAtIndex:row]; ++ ++ if(wantedSsid == QCFString::toQString(CFStringRef([apNetwork ssid]))) { ++ ++ if(!using8021X) { ++ SecKeychainAttribute attributes[3]; ++ ++ NSString *account = [apNetwork ssid]; ++ NSString *keyKind = @"AirPort network password"; ++ NSString *keyName = account; ++ ++ attributes[0].tag = kSecAccountItemAttr; ++ attributes[0].data = (void *)[account UTF8String]; ++ attributes[0].length = [account length]; ++ ++ attributes[1].tag = kSecDescriptionItemAttr; ++ attributes[1].data = (void *)[keyKind UTF8String]; ++ attributes[1].length = [keyKind length]; ++ ++ attributes[2].tag = kSecLabelItemAttr; ++ attributes[2].data = (void *)[keyName UTF8String]; ++ attributes[2].length = [keyName length]; ++ ++ SecKeychainAttributeList attributeList = {3,attributes}; ++ ++ SecKeychainSearchRef searchRef; ++ SecKeychainSearchCreateFromAttributes(NULL, kSecGenericPasswordItemClass, &attributeList, &searchRef); ++ ++ NSString *password = @""; ++ SecKeychainItemRef searchItem; ++ ++ if (SecKeychainSearchCopyNext(searchRef, &searchItem) == noErr) { ++ UInt32 realPasswordLength; ++ SecKeychainAttribute attributesW[8]; ++ attributesW[0].tag = kSecAccountItemAttr; ++ SecKeychainAttributeList listW = {1,attributesW}; ++ char *realPassword; ++ OSStatus status = SecKeychainItemCopyContent(searchItem, NULL, &listW, &realPasswordLength,(void **)&realPassword); ++ ++ if (status == noErr) { ++ if (realPassword != NULL) { ++ ++ QByteArray pBuf; ++ pBuf.resize(realPasswordLength); ++ pBuf.prepend(realPassword); ++ pBuf.insert(realPasswordLength,'\0'); ++ ++ password = [NSString stringWithUTF8String:pBuf]; ++ } ++ SecKeychainItemFreeContent(&listW, realPassword); ++ } ++ ++ CFRelease(searchItem); ++ } else { ++ qDebug() << "SecKeychainSearchCopyNext error"; ++ } ++ [params setValue: password forKey: kCWAssocKeyPassphrase]; ++ } // end using8021X ++ ++ ++ bool result = [wifiInterface associateToNetwork: apNetwork parameters:[NSDictionary dictionaryWithDictionary:params] error:&err]; ++ ++ if(!err) { ++ if(!result) { ++ emit connectionError(id, ConnectError); ++ } else { ++ return; ++ } ++ } else { ++ qDebug() <<"associate ERROR"<< QCFString::toQString(CFStringRef([err localizedDescription ])); ++ } ++ } ++ } //end scan network ++ } else { ++ qDebug() <<"scan ERROR"<< QCFString::toQString(CFStringRef([err localizedDescription ])); ++ } ++ emit connectionError(id, InterfaceLookupError); ++ } ++ ++ locker.unlock(); ++ emit connectionError(id, InterfaceLookupError); ++ [autoreleasepool release]; ++} ++ ++void QCoreWlanEngine::disconnectFromId(const QString &id) ++{ ++ QMutexLocker locker(&mutex); ++ ++ QString interfaceString = getInterfaceFromId(id); ++ NSAutoreleasePool *autoreleasepool = [[NSAutoreleasePool alloc] init]; ++ ++ CWInterface *wifiInterface = ++ [CWInterface interfaceWithName: (NSString *)QCFString::toCFStringRef(interfaceString)]; ++ ++ [wifiInterface disassociate]; ++ if ([[wifiInterface interfaceState]intValue] != kCWInterfaceStateInactive) { ++ locker.unlock(); ++ emit connectionError(id, DisconnectionError); ++ locker.relock(); ++ } ++ [autoreleasepool release]; ++} ++ ++void QCoreWlanEngine::requestUpdate() ++{ ++ scanThread->getUserConfigurations(); ++ doRequestUpdate(); ++} ++ ++void QCoreWlanEngine::doRequestUpdate() ++{ ++ QMutexLocker locker(&mutex); ++ ++ NSAutoreleasePool *autoreleasepool = [[NSAutoreleasePool alloc] init]; ++ ++ NSArray *wifiInterfaces = [CWInterface supportedInterfaces]; ++ for (uint row = 0; row < [wifiInterfaces count]; ++row) { ++ scanThread->interfaceName = QCFString::toQString(CFStringRef([wifiInterfaces objectAtIndex:row])); ++ scanThread->start(); ++ } ++ locker.unlock(); ++ [autoreleasepool release]; ++} ++ ++bool QCoreWlanEngine::isWifiReady(const QString &wifiDeviceName) ++{ ++ QMutexLocker locker(&mutex); ++ bool haswifi = false; ++ if(hasWifi) { ++ NSAutoreleasePool *autoreleasepool = [[NSAutoreleasePool alloc] init]; ++ CWInterface *defaultInterface = [CWInterface interfaceWithName: (NSString *)QCFString::toCFStringRef(wifiDeviceName)]; ++ if([defaultInterface power]) { ++ haswifi = true; ++ } ++ [autoreleasepool release]; ++ } ++ return haswifi; ++} ++ ++ ++QNetworkSession::State QCoreWlanEngine::sessionStateForId(const QString &id) ++{ ++ QMutexLocker locker(&mutex); ++ QNetworkConfigurationPrivatePointer ptr = accessPointConfigurations.value(id); ++ ++ if (!ptr) ++ return QNetworkSession::Invalid; ++ ++ if (!ptr->isValid) { ++ return QNetworkSession::Invalid; ++ } else if ((ptr->state & QNetworkConfiguration::Active) == QNetworkConfiguration::Active) { ++ return QNetworkSession::Connected; ++ } else if ((ptr->state & QNetworkConfiguration::Discovered) == ++ QNetworkConfiguration::Discovered) { ++ return QNetworkSession::Disconnected; ++ } else if ((ptr->state & QNetworkConfiguration::Defined) == QNetworkConfiguration::Defined) { ++ return QNetworkSession::NotAvailable; ++ } else if ((ptr->state & QNetworkConfiguration::Undefined) == ++ QNetworkConfiguration::Undefined) { ++ return QNetworkSession::NotAvailable; ++ } ++ ++ return QNetworkSession::Invalid; ++} ++ ++QNetworkConfigurationManager::Capabilities QCoreWlanEngine::capabilities() const ++{ ++ return QNetworkConfigurationManager::ForcedRoaming; ++} ++ ++void QCoreWlanEngine::startNetworkChangeLoop() ++{ ++ ++ SCDynamicStoreContext dynStoreContext = { 0, this/*(void *)storeSession*/, NULL, NULL, NULL }; ++ storeSession = SCDynamicStoreCreate(NULL, ++ CFSTR("networkChangeCallback"), ++ networkChangeCallback, ++ &dynStoreContext); ++ if (!storeSession ) { ++ qWarning() << "could not open dynamic store: error:" << SCErrorString(SCError()); ++ return; ++ } ++ ++ CFMutableArrayRef notificationKeys; ++ notificationKeys = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks); ++ CFMutableArrayRef patternsArray; ++ patternsArray = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks); ++ ++ CFStringRef storeKey; ++ storeKey = SCDynamicStoreKeyCreateNetworkGlobalEntity(NULL, ++ kSCDynamicStoreDomainState, ++ kSCEntNetIPv4); ++ CFArrayAppendValue(notificationKeys, storeKey); ++ CFRelease(storeKey); ++ ++ storeKey = SCDynamicStoreKeyCreateNetworkServiceEntity(NULL, ++ kSCDynamicStoreDomainState, ++ kSCCompAnyRegex, ++ kSCEntNetIPv4); ++ CFArrayAppendValue(patternsArray, storeKey); ++ CFRelease(storeKey); ++ ++ if (!SCDynamicStoreSetNotificationKeys(storeSession , notificationKeys, patternsArray)) { ++ qWarning() << "register notification error:"<< SCErrorString(SCError()); ++ CFRelease(storeSession ); ++ CFRelease(notificationKeys); ++ CFRelease(patternsArray); ++ return; ++ } ++ CFRelease(notificationKeys); ++ CFRelease(patternsArray); ++ ++ runloopSource = SCDynamicStoreCreateRunLoopSource(NULL, storeSession , 0); ++ if (!runloopSource) { ++ qWarning() << "runloop source error:"<< SCErrorString(SCError()); ++ CFRelease(storeSession ); ++ return; ++ } ++ ++ CFRunLoopAddSource(CFRunLoopGetCurrent(), runloopSource, kCFRunLoopDefaultMode); ++ return; ++} ++ ++QNetworkSessionPrivate *QCoreWlanEngine::createSessionBackend() ++{ ++ return new QNetworkSessionPrivateImpl; ++} ++ ++QNetworkConfigurationPrivatePointer QCoreWlanEngine::defaultConfiguration() ++{ ++ return QNetworkConfigurationPrivatePointer(); ++} ++ ++bool QCoreWlanEngine::requiresPolling() const ++{ ++ return true; ++} ++ ++void QCoreWlanEngine::networksChanged() ++{ ++ QMutexLocker locker(&mutex); ++ ++ QStringList previous = accessPointConfigurations.keys(); ++ ++ QList foundConfigurations = scanThread->getConfigurations(); ++ while (!foundConfigurations.isEmpty()) { ++ QNetworkConfigurationPrivate *cpPriv = foundConfigurations.takeFirst(); ++ ++ previous.removeAll(cpPriv->id); ++ ++ if (accessPointConfigurations.contains(cpPriv->id)) { ++ QNetworkConfigurationPrivatePointer ptr = accessPointConfigurations.value(cpPriv->id); ++ ++ bool changed = false; ++ ++ ptr->mutex.lock(); ++ ++ if (ptr->isValid != cpPriv->isValid) { ++ ptr->isValid = cpPriv->isValid; ++ changed = true; ++ } ++ ++ if (ptr->name != cpPriv->name) { ++ ptr->name = cpPriv->name; ++ changed = true; ++ } ++ ++ if (ptr->bearerType != cpPriv->bearerType) { ++ ptr->bearerType = cpPriv->bearerType; ++ changed = true; ++ } ++ ++ if (ptr->state != cpPriv->state) { ++ ptr->state = cpPriv->state; ++ changed = true; ++ } ++ ++ ptr->mutex.unlock(); ++ ++ if (changed) { ++ locker.unlock(); ++ emit configurationChanged(ptr); ++ locker.relock(); ++ } ++ ++ delete cpPriv; ++ } else { ++ QNetworkConfigurationPrivatePointer ptr(cpPriv); ++ ++ accessPointConfigurations.insert(ptr->id, ptr); ++ ++ locker.unlock(); ++ emit configurationAdded(ptr); ++ locker.relock(); ++ } ++ } ++ ++ while (!previous.isEmpty()) { ++ QNetworkConfigurationPrivatePointer ptr = ++ accessPointConfigurations.take(previous.takeFirst()); ++ ++ locker.unlock(); ++ emit configurationRemoved(ptr); ++ locker.relock(); ++ } ++ ++ locker.unlock(); ++ emit updateCompleted(); ++ ++} ++ ++quint64 QCoreWlanEngine::bytesWritten(const QString &id) ++{ ++ QMutexLocker locker(&mutex); ++ const QString interfaceStr = getInterfaceFromId(id); ++ return getBytes(interfaceStr,false); ++} ++ ++quint64 QCoreWlanEngine::bytesReceived(const QString &id) ++{ ++ QMutexLocker locker(&mutex); ++ const QString interfaceStr = getInterfaceFromId(id); ++ return getBytes(interfaceStr,true); ++} ++ ++quint64 QCoreWlanEngine::startTime(const QString &identifier) ++{ ++ QMutexLocker locker(&mutex); ++ NSAutoreleasePool *autoreleasepool = [[NSAutoreleasePool alloc] init]; ++ quint64 timestamp = 0; ++ ++ NSString *filePath = @"/Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist"; ++ NSDictionary* plistDict = [[[NSDictionary alloc] initWithContentsOfFile:filePath] autorelease]; ++ if(plistDict == nil) ++ return timestamp; ++ NSString *input = @"KnownNetworks"; ++ NSString *timeStampStr = @"_timeStamp"; ++ ++ NSString *ssidStr = @"SSID_STR"; ++ ++ for (id key in plistDict) { ++ if ([input isEqualToString:key]) { ++ ++ NSDictionary *knownNetworksDict = [plistDict objectForKey:key]; ++ if(knownNetworksDict == nil) ++ return timestamp; ++ for (id networkKey in knownNetworksDict) { ++ bool isFound = false; ++ NSDictionary *itemDict = [knownNetworksDict objectForKey:networkKey]; ++ if(itemDict == nil) ++ return timestamp; ++ NSInteger dictSize = [itemDict count]; ++ id objects[dictSize]; ++ id keys[dictSize]; ++ ++ [itemDict getObjects:objects andKeys:keys]; ++ bool ok = false; ++ for(int i = 0; i < dictSize; i++) { ++ if([ssidStr isEqualToString:keys[i]]) { ++ const QString ident = QString::number(qHash(QLatin1String("corewlan:") + QCFString::toQString(CFStringRef(objects[i])))); ++ if(ident == identifier) { ++ ok = true; ++ } ++ } ++ if(ok && [timeStampStr isEqualToString:keys[i]]) { ++ timestamp = (quint64)[objects[i] timeIntervalSince1970]; ++ isFound = true; ++ break; ++ } ++ } ++ if(isFound) ++ break; ++ } ++ } ++ } ++ [autoreleasepool release]; ++ return timestamp; ++} ++ ++quint64 QCoreWlanEngine::getBytes(const QString &interfaceName, bool b) ++{ ++ struct ifaddrs *ifAddressList, *ifAddress; ++ struct if_data *if_data; ++ ++ quint64 bytes = 0; ++ ifAddressList = nil; ++ if(getifaddrs(&ifAddressList) == 0) { ++ for(ifAddress = ifAddressList; ifAddress; ifAddress = ifAddress->ifa_next) { ++ if(interfaceName == ifAddress->ifa_name) { ++ if_data = (struct if_data*)ifAddress->ifa_data; ++ if(b) { ++ bytes = if_data->ifi_ibytes; ++ break; ++ } else { ++ bytes = if_data->ifi_obytes; ++ break; ++ } ++ } ++ } ++ freeifaddrs(ifAddressList); ++ } ++ return bytes; ++} ++ ++QT_END_NAMESPACE From 8c060691d732c95ca5868a26334a1c0381e0f586 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Thu, 5 Dec 2013 12:21:10 -0500 Subject: [PATCH 23/66] Update Mac deployment target to make it build on 10.9, apply Qt4 patches --- scripts/macosx-build-dependencies.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/macosx-build-dependencies.sh b/scripts/macosx-build-dependencies.sh index 19c97097..d4ca1f7c 100755 --- a/scripts/macosx-build-dependencies.sh +++ b/scripts/macosx-build-dependencies.sh @@ -24,7 +24,7 @@ BASEDIR=$PWD/../libraries OPENSCADDIR=$PWD SRCDIR=$BASEDIR/src DEPLOYDIR=$BASEDIR/install -MAC_OSX_VERSION_MIN=10.6 +MAC_OSX_VERSION_MIN=10.7 OPTION_32BIT=false OPTION_LLVM=false OPTION_CLANG=false @@ -54,6 +54,9 @@ build_qt() fi tar xzf qt-everywhere-opensource-src-$version.tar.gz cd qt-everywhere-opensource-src-$version + patch -p0 < $OPENSCADDIR/patches/qt4/patch-src_corelib_global_qglobal.h.diff + patch -p0 < $OPENSCADDIR/patches/qt4/patch-libtiff.diff + patch -p0 < $OPENSCADDIR/patches/qt4/patch-src_plugins_bearer_corewlan_qcorewlanengine.mm.diff if $USING_CLANG; then # FIX for clang sed -i "" -e "s/::TabletProximityRec/TabletProximityRec/g" src/gui/kernel/qt_cocoa_helpers_mac_p.h @@ -220,7 +223,7 @@ build_boost() BOOST_TOOLSET="toolset=clang" echo "using clang ;" >> tools/build/v2/user-config.jam fi - ./b2 -d+2 $BOOST_TOOLSET cflags="-mmacosx-version-min=$MAC_OSX_VERSION_MIN -arch x86_64 $BOOST_EXTRA_FLAGS" linkflags="-mmacosx-version-min=$MAC_OSX_VERSION_MIN -arch x86_64 $BOOST_EXTRA_FLAGS -headerpad_max_install_names" install + ./b2 -j6 -d+2 $BOOST_TOOLSET cflags="-mmacosx-version-min=$MAC_OSX_VERSION_MIN -arch x86_64 $BOOST_EXTRA_FLAGS" linkflags="-mmacosx-version-min=$MAC_OSX_VERSION_MIN -arch x86_64 $BOOST_EXTRA_FLAGS -headerpad_max_install_names" install install_name_tool -id $DEPLOYDIR/lib/libboost_thread.dylib $DEPLOYDIR/lib/libboost_thread.dylib install_name_tool -change libboost_system.dylib $DEPLOYDIR/lib/libboost_system.dylib $DEPLOYDIR/lib/libboost_thread.dylib install_name_tool -change libboost_chrono.dylib $DEPLOYDIR/lib/libboost_chrono.dylib $DEPLOYDIR/lib/libboost_thread.dylib From ac7b37a3d67662b99adb2648945f7c972624344c Mon Sep 17 00:00:00 2001 From: Brody Kenrick Date: Fri, 6 Dec 2013 09:56:22 +1100 Subject: [PATCH 24/66] Update comments/messages for CGAL source of bad boost libraries Add comments and change to a status instead of a warning (as we recover nicely and it is a known issue in CGAL @ https://bugs.launchpad.net/ubuntu/+source/cgal/+bug/1242111) --- tests/CMakeLists.txt | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index fd5097a8..65eefcda 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -376,23 +376,24 @@ if("${CGAL_MAJOR_VERSION}.${CGAL_MINOR_VERSION}" VERSION_LESS 3.6) message(FATAL_ERROR "CGAL >= 3.6 required") endif() inclusion(CGAL_DIR CGAL_INCLUDE_DIRS) - -#Get rid of bad libraries suggested for BOOST dependencies (they don't exist on some machines and cause build failures). -#/usr/lib/libboost_thread.so;/usr/lib/libboost_system.so; +#Remove bad BOOST libraries from CGAL 3rd party dependencies when they don't exist (such as on 64-bit Ubuntu 13.10). +#Libs of concern are /usr/lib/libboost_thread.so;/usr/lib/libboost_system.so; +#Confirmed bug in CGAL @ https://bugs.launchpad.net/ubuntu/+source/cgal/+bug/1242111 string(FIND "${CGAL_3RD_PARTY_LIBRARIES}" "/usr/lib/libboost_system.so" FIND_POSITION ) if(NOT "-1" STREQUAL ${FIND_POSITION} ) -if(NOT EXISTS "/usr/lib/libboost_system.so") - MESSAGE( WARNING "CGAL_3RD_PARTY_LIBRARIES:Found erroneous /usr/lib/libboost_system.so -- stripping" ) - string(REPLACE "/usr/lib/libboost_system.so" "" CGAL_3RD_PARTY_LIBRARIES ${CGAL_3RD_PARTY_LIBRARIES}) -endif() + if(NOT EXISTS "/usr/lib/libboost_system.so") + MESSAGE( STATUS "CGAL_3RD_PARTY_LIBRARIES:Removing non-existent /usr/lib/libboost_system.so" ) + string(REPLACE "/usr/lib/libboost_system.so" "" CGAL_3RD_PARTY_LIBRARIES ${CGAL_3RD_PARTY_LIBRARIES}) + endif() endif() string(FIND "${CGAL_3RD_PARTY_LIBRARIES}" "/usr/lib/libboost_thread.so" FIND_POSITION ) if(NOT "-1" STREQUAL ${FIND_POSITION} ) -if(NOT EXISTS "/usr/lib/libboost_thread.so") - MESSAGE( WARNING "CGAL_3RD_PARTY_LIBRARIES:Found erroneous /usr/lib/libboost_thread.so -- stripping" ) - string(REPLACE "/usr/lib/libboost_thread.so" "" CGAL_3RD_PARTY_LIBRARIES ${CGAL_3RD_PARTY_LIBRARIES}) -endif() + if(NOT EXISTS "/usr/lib/libboost_thread.so") + MESSAGE( STATUS "CGAL_3RD_PARTY_LIBRARIES:Removing non-existent /usr/lib/libboost_thread.so" ) + string(REPLACE "/usr/lib/libboost_thread.so" "" CGAL_3RD_PARTY_LIBRARIES ${CGAL_3RD_PARTY_LIBRARIES}) + endif() endif() + if(${CMAKE_CXX_COMPILER} MATCHES ".*clang.*" AND NOT ${CGAL_CXX_FLAGS_INIT} STREQUAL "" ) string(REPLACE "-frounding-math" "" CGAL_CXX_FLAGS_INIT ${CGAL_CXX_FLAGS_INIT}) string(REPLACE "--param=ssp-buffer-size=4" "" CGAL_CXX_FLAGS_INIT ${CGAL_CXX_FLAGS_INIT}) From 38a342215970396a5590281cd66d0ca9c9ab7e98 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Fri, 6 Dec 2013 00:39:21 -0500 Subject: [PATCH 25/66] #559 fixes for 10.9 --- tests/CMakeLists.txt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0477a45d..b84775b8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,6 +1,6 @@ # instructions - see ../doc/testing.txt -# set(DEBUG_OSCD 1) # print debug info during cmake +#set(DEBUG_OSCD 1) # print debug info during cmake cmake_minimum_required(VERSION 2.8) if("${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}.${CMAKE_PATCH_VERSION}" VERSION_GREATER 2.8.3) @@ -14,9 +14,16 @@ include(CMakeParseArguments.cmake) # Detect Lion and force gcc IF (APPLE) + # Somehow, since we build dependencies for 10.7, we need to also build executables + # for 10.7. This used to not be necessary, but since 10.9 it apparently is.. + SET(CMAKE_OSX_DEPLOYMENT_TARGET 10.7) EXECUTE_PROCESS(COMMAND sw_vers -productVersion OUTPUT_VARIABLE MACOSX_VERSION) - IF (NOT ${MACOSX_VERSION} VERSION_LESS "10.8.0") - message("Detected Mountain Lion (10.8) or later") + IF (NOT ${MACOSX_VERSION} VERSION_LESS "10.9.0") + message("Detected Maverick (10.9) or later") + set(CMAKE_C_COMPILER "clang") + set(CMAKE_CXX_COMPILER "clang++") + ELSEIF (NOT ${MACOSX_VERSION} VERSION_LESS "10.8.0") + message("Detected Mountain Lion (10.8)") set(CMAKE_C_COMPILER "clang") set(CMAKE_CXX_COMPILER "clang++") ELSEIF (NOT ${MACOSX_VERSION} VERSION_LESS "10.7.0") From e3317ecc64659ae8be2a7b02d4df17dad6d875db Mon Sep 17 00:00:00 2001 From: "David Eccles (gringer)" Date: Fri, 6 Dec 2013 13:14:09 +1300 Subject: [PATCH 26/66] Fail if any polygon points for rotate_extrude are less than 0 --- src/PolySetCGALEvaluator.cc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/PolySetCGALEvaluator.cc b/src/PolySetCGALEvaluator.cc index bc9206f3..e5eba2b9 100644 --- a/src/PolySetCGALEvaluator.cc +++ b/src/PolySetCGALEvaluator.cc @@ -457,7 +457,13 @@ PolySet *PolySetCGALEvaluator::rotateDxfData(const RotateExtrudeNode &node, DxfD { double max_x = 0; for (size_t j = 0; j < dxf.paths[i].indices.size(); j++) { - max_x = fmax(max_x, dxf.points[dxf.paths[i].indices[j]][0]); + double point_x = dxf.points[dxf.paths[i].indices[j]][0]; + if(point_x < 0){ + PRINT("ERROR: all points for rotate_extrude() must have non-negative X coordinates"); + PRINT((boost::format("[Point %d on path %d has X coordinate %f]") % j % i % point_x).str()); + return NULL; + } + max_x = fmax(max_x, point_x); } int fragments = get_fragments_from_r(max_x, node.fn, node.fs, node.fa); From ede5c4b6882692dc28fc7017c4aeb87ec9222f8e Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Fri, 6 Dec 2013 00:52:51 -0500 Subject: [PATCH 27/66] delete ps when short-circuiting return, no need for explicit boost::format --- src/PolySetCGALEvaluator.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/PolySetCGALEvaluator.cc b/src/PolySetCGALEvaluator.cc index e5eba2b9..599fd7f5 100644 --- a/src/PolySetCGALEvaluator.cc +++ b/src/PolySetCGALEvaluator.cc @@ -458,9 +458,10 @@ PolySet *PolySetCGALEvaluator::rotateDxfData(const RotateExtrudeNode &node, DxfD double max_x = 0; for (size_t j = 0; j < dxf.paths[i].indices.size(); j++) { double point_x = dxf.points[dxf.paths[i].indices[j]][0]; - if(point_x < 0){ + if (point_x < 0) { PRINT("ERROR: all points for rotate_extrude() must have non-negative X coordinates"); - PRINT((boost::format("[Point %d on path %d has X coordinate %f]") % j % i % point_x).str()); + PRINTB("[Point %d on path %d has X coordinate %f]", j % i % point_x); + delete ps; return NULL; } max_x = fmax(max_x, point_x); From a0d8cbe692a5a474a7bc284cded4a01894b25c2a Mon Sep 17 00:00:00 2001 From: Brody Kenrick Date: Fri, 6 Dec 2013 17:46:52 +1100 Subject: [PATCH 28/66] Add in missed glib build dependencies for OS X and unix --- scripts/macosx-build-dependencies.sh | 25 +++++++++++++++++++++++++ scripts/uni-build-dependencies.sh | 25 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/scripts/macosx-build-dependencies.sh b/scripts/macosx-build-dependencies.sh index 19c97097..81985bc7 100755 --- a/scripts/macosx-build-dependencies.sh +++ b/scripts/macosx-build-dependencies.sh @@ -282,6 +282,30 @@ build_glew() make GLEW_DEST=$DEPLOYDIR CC=$CC CFLAGS.EXTRA="-no-cpp-precomp -dynamic -fno-common -mmacosx-version-min=$MAC_OSX_VERSION_MIN $GLEW_EXTRA_FLAGS -arch x86_64" LDFLAGS.EXTRA="-mmacosx-version-min=$MAC_OSX_VERSION_MIN $GLEW_EXTRA_FLAGS -arch x86_64" STRIP= install } +build_glib2() +{ + version="$1" + maj_min_version="${version%.*}" #Drop micro + + if [ -e $DEPLOYDIR/lib/glib-2.0 ]; then + echo "glib2 already installed. not building" + return + fi + + echo "Building glib2 $version..." + cd "$BASEDIR"/src + rm -rf "glib-$version" + if [ ! -f "glib-$version.tar.xz" ]; then + curl --insecure -LO "http://ftp.gnome.org/pub/gnome/sources/glib/$maj_min_version/glib-$version.tar.xz" + fi + tar xJf "glib-$version.tar.xz" + cd "glib-$version" + + ./configure --prefix="$DEPLOYDIR" + make -j$NUMCPU + make install +} + build_opencsg() { version=$1 @@ -446,6 +470,7 @@ build_boost 1.54.0 # NB! For CGAL, also update the actual download URL in the function build_cgal 4.3 build_glew 1.10.0 +build_glib2 2.38.1 build_opencsg 1.3.2 if $OPTION_DEPLOY; then # build_sparkle andymatuschak 0ed83cf9f2eeb425d4fdd141c01a29d843970c20 diff --git a/scripts/uni-build-dependencies.sh b/scripts/uni-build-dependencies.sh index 8d912c35..ba328b7e 100755 --- a/scripts/uni-build-dependencies.sh +++ b/scripts/uni-build-dependencies.sh @@ -409,6 +409,31 @@ build_glew() GLEW_DEST=$DEPLOYDIR $MAKER install } +build_glib2() +{ + version="$1" + maj_min_version="${version%.*}" #Drop micro + + if [ -e $DEPLOYDIR/lib/glib-2.0 ]; then +echo "glib2 already installed. not building" + return +fi + +echo "Building glib2 $version..." + cd "$BASEDIR"/src + rm -rf "glib-$version" + if [ ! -f "glib-$version.tar.xz" ]; then +curl --insecure -LO "http://ftp.gnome.org/pub/gnome/sources/glib/$maj_min_version/glib-$version.tar.xz" + fi +tar xJf "glib-$version.tar.xz" + cd "glib-$version" + + ./configure --prefix="$DEPLOYDIR" + make -j$NUMCPU + make install + +} + build_opencsg() { if [ -e $DEPLOYDIR/lib/libopencsg.so ]; then From d7d5bea7363703c76b9787598304bfc838e893ee Mon Sep 17 00:00:00 2001 From: Brody Kenrick Date: Fri, 6 Dec 2013 18:33:42 +1100 Subject: [PATCH 29/66] Add specific tests for unicode len() --- testdata/scad/misc/string-unicode.scad | 10 +++++++++- tests/regression/echotest/string-unicode-expected.echo | 3 +++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/testdata/scad/misc/string-unicode.scad b/testdata/scad/misc/string-unicode.scad index d8e3e5c9..1386d63d 100644 --- a/testdata/scad/misc/string-unicode.scad +++ b/testdata/scad/misc/string-unicode.scad @@ -1,3 +1,12 @@ +//Test length reporting +text_1bytes_len = "1234"; +text_2bytes_len = "ЛЛЛЛ"; +text_4bytes_len = "🂡🂱🃁🃑"; + +echo( "text_1bytes_len = ", text_1bytes_len, " len = ", len(text_1bytes_len) ); +echo( "text_2bytes_len = ", text_2bytes_len, " len = ", len(text_2bytes_len) ); +echo( "text_4bytes_len = ", text_4bytes_len, " len = ", len(text_4bytes_len) ); + //Test how well arrays of unicode string are accessed. texts_array = [ @@ -33,4 +42,3 @@ echo( "Past end of unicode only 4-byte ", text_4bytes[len(text_4bytes)] ); echo( "Past end of both 2-byte ", text_2bytes[ len(text_2bytes) * 2 ] ); echo( "Past end of both 4-byte ", text_4bytes[ len(text_4bytes) * 4 ] ); - diff --git a/tests/regression/echotest/string-unicode-expected.echo b/tests/regression/echotest/string-unicode-expected.echo index b4b848fd..a1cd3bec 100644 --- a/tests/regression/echotest/string-unicode-expected.echo +++ b/tests/regression/echotest/string-unicode-expected.echo @@ -1,3 +1,6 @@ +ECHO: "text_1bytes_len = ", "1234", " len = ", 4 +ECHO: "text_2bytes_len = ", "ЛЛЛЛ", " len = ", 4 +ECHO: "text_4bytes_len = ", "🂡🂱🃁🃑", " len = ", 4 ECHO: "[", 0, "] = ", "DEADBEEF", " of len=", 8, ":" ECHO: " [", 0, "]=", "D" ECHO: " [", 1, "]=", "E" From 40ae8c7b343fc5dafc8c9bdc7d8a63f6e8032fca Mon Sep 17 00:00:00 2001 From: a-e-m Date: Sat, 7 Dec 2013 14:38:22 -0800 Subject: [PATCH 30/66] Add html report upload to test_pretty_print.py, removed wiki support --- tests/test_pretty_print.py | 878 ++++++++++++++++--------------------- 1 file changed, 381 insertions(+), 497 deletions(-) diff --git a/tests/test_pretty_print.py b/tests/test_pretty_print.py index a31b1a86..c26b919d 100755 --- a/tests/test_pretty_print.py +++ b/tests/test_pretty_print.py @@ -25,544 +25,428 @@ # todo # -# 1. Note: Wiki code is deprecated. All future development should move to -# create html output (or even xml output). Wiki design was based on the -# wrong assumption of easily accessible public wiki servers with -# auto-upload scripts available. wiki code should be removed and code -# simplified. -# -# to still use wiki, use args '--wiki' and/or '--wiki-upload' -# -# 2. why is hash differing +# 1. why is hash differing -import string,sys,re,os,hashlib,subprocess,textwrap,time,platform +import string +import sys +import re +import os +import hashlib +import subprocess +import time +import platform def tryread(filename): - data = None - try: - f = open(filename,'rb') - data = f.read() - f.close() - except Exception, e: - print 'couldn\'t open ',filename - print type(e), e - return data + data = None + try: + f = open(filename,'rb') + data = f.read() + f.close() + except Exception as e: + print 'couldn\'t open ',filename + print type(e), e + return data -def trysave(filename,data): - dir = os.path.dirname(filename) - try: - if not os.path.isdir(dir): - if not dir == '': - debug( 'creating' + dir) - os.mkdir(dir) - f=open(filename,'wb') - f.write(data) - f.close() - except Exception, e: - print 'problem writing to',filename - print type(e), e - return None - return True +def trysave(filename, data): + dir = os.path.dirname(filename) + try: + if not os.path.isdir(dir): + if not dir == '': + debug( 'creating' + dir) + os.mkdir(dir) + f=open(filename,'wb') + f.write(data) + f.close() + except Exception as e: + print 'problem writing to',filename + print type(e), e + return None + return True -def ezsearch(pattern,str): - x = re.search(pattern,str,re.DOTALL|re.MULTILINE) - if x and len(x.groups())>0: return x.group(1).strip() - return '' - +def ezsearch(pattern, str): + x = re.search(pattern,str,re.DOTALL|re.MULTILINE) + if x and len(x.groups())>0: return x.group(1).strip() + return '' + def read_gitinfo(): - # won't work if run from outside of branch. - try: - data = subprocess.Popen(['git','remote','-v'],stdout=subprocess.PIPE).stdout.read() - origin = ezsearch('^origin *?(.*?)\(fetch.*?$',data) - upstream = ezsearch('^upstream *?(.*?)\(fetch.*?$',data) - data = subprocess.Popen(['git','branch'],stdout=subprocess.PIPE).stdout.read() - branch = ezsearch('^\*(.*?)$',data) - out = 'Git branch: ' + branch + ' from origin ' + origin + '\n' - out += 'Git upstream: ' + upstream + '\n' - except: - out = 'Problem running git' - return out + # won't work if run from outside of branch. + try: + data = subprocess.Popen(['git', 'remote', '-v'], stdout=subprocess.PIPE).stdout.read() + origin = ezsearch('^origin *?(.*?)\(fetch.*?$', data) + upstream = ezsearch('^upstream *?(.*?)\(fetch.*?$', data) + data = subprocess.Popen(['git', 'branch'], stdout=subprocess.PIPE).stdout.read() + branch = ezsearch('^\*(.*?)$', data) + out = 'Git branch: ' + branch + ' from origin ' + origin + '\n' + out += 'Git upstream: ' + upstream + '\n' + except: + out = 'Problem running git' + return out def read_sysinfo(filename): - data = tryread(filename) - if not data: - sinfo = platform.sys.platform - sinfo += '\nsystem cannot create offscreen GL framebuffer object' - sinfo += '\nsystem cannot create GL based images' - sysid = platform.sys.platform+'_no_GL_renderer' - return sinfo, sysid + data = tryread(filename) + if not data: + sinfo = platform.sys.platform + sinfo += '\nsystem cannot create offscreen GL framebuffer object' + sinfo += '\nsystem cannot create GL based images' + sysid = platform.sys.platform+'_no_GL_renderer' + return sinfo, sysid - machine = ezsearch('Machine:(.*?)\n',data) - machine = machine.replace(' ','-').replace('/','-') + machine = ezsearch('Machine:(.*?)\n',data) + machine = machine.replace(' ','-').replace('/','-') - osinfo = ezsearch('OS info:(.*?)\n',data) - osplain = osinfo.split(' ')[0].strip().replace('/','-') - if 'windows' in osinfo.lower(): osplain = 'win' + osinfo = ezsearch('OS info:(.*?)\n',data) + osplain = osinfo.split(' ')[0].strip().replace('/','-') + if 'windows' in osinfo.lower(): + osplain = 'win' - renderer = ezsearch('GL Renderer:(.*?)\n',data) - tmp = renderer.split(' ') - tmp = string.join(tmp[0:3],'-') - tmp = tmp.split('/')[0] - renderer = tmp + renderer = ezsearch('GL Renderer:(.*?)\n',data) + tmp = renderer.split(' ') + tmp = string.join(tmp[0:3],'-') + tmp = tmp.split('/')[0] + renderer = tmp - data += read_gitinfo() + data += read_gitinfo() - data += 'Image comparison: ImageMagick' + data += 'Image comparison: ImageMagick' - data = data.strip() + data = data.strip() - # create 4 letter hash and stick on end of sysid - nondate_data = re.sub("\n.*?ompile date.*?\n","\n",data).strip() - hexhash = hashlib.md5() - hexhash.update(nondate_data) - hexhash = hexhash.hexdigest()[-4:].upper() - hash = '' - for c in hexhash: hash += chr(ord(c)+97-48) + # create 4 letter hash and stick on end of sysid + nondate_data = re.sub("\n.*?ompile date.*?\n", "\n", data).strip() + hexhash = hashlib.md5(nondate_data).hexdigest()[-4:].upper() + hash_ = ''.join(chr(ord(c) + 97 - 48) for c in hexhash) - sysid = osplain + '_' + machine + '_' + renderer + '_' + hash - sysid = sysid.replace('(','_').replace(')','_') - sysid = sysid.lower() + sysid = '_'.join([osplain, machine, renderer, hash_]) + sysid = sysid.replace('(', '_').replace(')', '_') + sysid = sysid.lower() - return data, sysid + return data, sysid class Test: - def __init__(self,fullname,time,passed,output,type,actualfile,expectedfile,scadfile,log): - self.fullname,self.time,self.passed,self.output = \ - fullname, time, passed, output - self.type, self.actualfile, self.expectedfile, self.scadfile = \ - type, actualfile, expectedfile, scadfile - self.fulltestlog = log - - def __str__(self): - x = 'fullname: ' + self.fullname - x+= '\nactualfile: ' + self.actualfile - x+= '\nexpectedfile: ' + self.expectedfile - x+= '\ntesttime: ' + self.time - x+= '\ntesttype: ' + self.type - x+= '\npassed: ' + str(self.passed) - x+= '\nscadfile: ' + self.scadfile - x+= '\noutput bytes: ' + str(len(self.output)) - x+= '\ntestlog bytes: ' + str(len(self.fulltestlog)) - x+= '\n' - return x + def __init__(self, fullname, subpr, passed, output, type, actualfile, + expectedfile, scadfile, log): + self.fullname, self.time = fullname, time + self.passed, self.output = passed, output + self.type, self.actualfile = type, actualfile + self.expectedfile, self.scadfile = expectedfile, scadfile + self.fulltestlog = log + + def __str__(self): + x = 'fullname: ' + self.fullname + x+= '\nactualfile: ' + self.actualfile + x+= '\nexpectedfile: ' + self.expectedfile + x+= '\ntesttime: ' + self.time + x+= '\ntesttype: ' + self.type + x+= '\npassed: ' + str(self.passed) + x+= '\nscadfile: ' + self.scadfile + x+= '\noutput bytes: ' + str(len(self.output)) + x+= '\ntestlog bytes: ' + str(len(self.fulltestlog)) + x+= '\n' + return x def parsetest(teststring): - patterns = ["Test:(.*?)\n", # fullname - "Test time =(.*?) sec\n", - "Test time.*?Test (Passed)", # pass/fail - "Output:(.*?)", - 'Command:.*?-s" "(.*?)"', # type - "^ actual .*?:(.*?)\n", - "^ expected .*?:(.*?)\n", - 'Command:.*?(testdata.*?)"' # scadfile - ] - hits = map( lambda pattern: ezsearch(pattern,teststring), patterns ) - test = Test(hits[0],hits[1],hits[2]=='Passed',hits[3],hits[4],hits[5],hits[6],hits[7],teststring) - if len(test.actualfile) > 0: test.actualfile_data = tryread(test.actualfile) - if len(test.expectedfile) > 0: test.expectedfile_data = tryread(test.expectedfile) - return test + patterns = ["Test:(.*?)\n", # fullname + "Test time =(.*?) sec\n", + "Test time.*?Test (Passed)", # pass/fail + "Output:(.*?)", + 'Command:.*?-s" "(.*?)"', # type + "^ actual .*?:(.*?)\n", + "^ expected .*?:(.*?)\n", + 'Command:.*?(testdata.*?)"' # scadfile + ] + hits = map( lambda pattern: ezsearch(pattern, teststring), patterns) + test = Test(hits[0], hits[1], hits[2]=='Passed', hits[3], hits[4], hits[5], + hits[6], hits[7], teststring) + if len(test.actualfile) > 0: + test.actualfile_data = tryread(test.actualfile) + if len(test.expectedfile) > 0: + test.expectedfile_data = tryread(test.expectedfile) + return test def parselog(data): - startdate = ezsearch('Start testing: (.*?)\n',data) - enddate = ezsearch('End testing: (.*?)\n',data) - pattern = '([0-9]*/[0-9]* Testing:.*?time elapsed.*?\n)' - test_chunks = re.findall(pattern,data,re.S) - tests = map( parsetest, test_chunks ) - tests = sorted(tests, key = lambda t:t.passed) - return startdate, tests, enddate + startdate = ezsearch('Start testing: (.*?)\n', data) + enddate = ezsearch('End testing: (.*?)\n', data) + pattern = '([0-9]*/[0-9]* Testing:.*?time elapsed.*?\n)' + test_chunks = re.findall(pattern,data, re.S) + tests = map( parsetest, test_chunks ) + tests = sorted(tests, key = lambda t: t.passed) + return startdate, tests, enddate def load_makefiles(builddir): - filelist = [] - for root, dirs, files in os.walk(builddir): - for fname in files: filelist += [ os.path.join(root, fname) ] - files = filter(lambda x: 'build.make' in os.path.basename(x), filelist) - files += filter(lambda x: 'flags.make' in os.path.basename(x), filelist) - files = filter(lambda x: 'esting' not in x and 'emporary' not in x, files) - result = {} - for fname in files: - result[fname.replace(builddir,'')] = open(fname,'rb').read() - return result - -def wikify_filename(fname, wiki_rootpath, sysid): - wikifname = fname.replace('/','_').replace('\\','_').strip('.') - return wiki_rootpath + '_' + sysid + '_' + wikifname - -def towiki(wiki_rootpath, startdate, tests, enddate, sysinfo, sysid, makefiles): - - wiki_template = """ -

    [[WIKI_ROOTPATH]] test run report

    - -'''Sysid''': SYSID - -'''Result summary''': NUMPASSED / NUMTESTS tests passed ( PERCENTPASSED % )
    - -'''System info''': -
    -SYSINFO
    -
    - -start time: STARTDATE
    -end time : ENDDATE
    - -'''Image tests''' - - -{| border=1 cellspacing=0 cellpadding=1 -|- -| colspan=2 | FTESTNAME -|- -| Expected image || Actual image -|- -| [[File:EXPECTEDFILE|250px]] || ACTUALFILE_WIKI -|} - -
    -TESTLOG
    -
    + filelist = [] + for root, dirs, files in os.walk(builddir): + for fname in files: filelist += [ os.path.join(root, fname) ] + files = [file for file in filelist if 'build.make' in os.path.basename(file) + or 'flags.make' in os.path.basename(file)] + files = [file for file in files if 'esting' not in file and 'emporary' not in file] + result = {} + for fname in files: + result[fname.replace(builddir, '')] = tryread(fname) + return result +def png_encode64(fname, width=250, data=None): + # en.wikipedia.org/wiki/Data_URI_scheme + data = data or tryread(fname) or '' + data_uri = data.encode('base64').replace('\n', '') + tag = '''''' + if data == '': + alt = 'alt="error: no image generated" ' + else: + alt = 'alt="openscad_test_image" ' + tag %= (data_uri, width, alt) + return tag -
    - - -'''Text tests''' - - -{|border=1 cellspacing=0 cellpadding=1 -|- -| FTESTNAME -|} - -
    -TESTLOG
    -
    - - -
    - -'''build.make and flags.make''' - -*[[MAKEFILE_NAME]] - -""" - txtpages = {} - imgs = {} - passed_tests = filter(lambda x: x.passed, tests) - failed_tests = filter(lambda x: not x.passed, tests) - - tests_to_report = failed_tests - if include_passed: tests_to_report = tests - - try: percent = str(int(100.0*len(passed_tests) / len(tests))) - except ZeroDivisionError: percent = 'n/a' - s = wiki_template - repeat1 = ezsearch('(.*?)',s) - repeat2 = ezsearch('(.*?)',s) - repeat3 = ezsearch('(.*?)',s) - dic = { 'STARTDATE': startdate, 'ENDDATE': enddate, 'WIKI_ROOTPATH': wiki_rootpath, - 'SYSINFO': sysinfo, 'SYSID':sysid, - 'NUMTESTS':len(tests), 'NUMPASSED':len(passed_tests), 'PERCENTPASSED':percent } - for key in dic.keys(): - s = s.replace(key,str(dic[key])) - - for t in tests_to_report: - if t.type in ('txt', 'ast', 'csg', 'term', 'echo'): - newchunk = re.sub('FTESTNAME',t.fullname,repeat2) - newchunk = newchunk.replace('TESTLOG',t.fulltestlog) - s = s.replace(repeat2, newchunk+repeat2) - elif t.type=='png': - tmp = t.actualfile.replace(builddir,'') - wikiname_a = wikify_filename(tmp,wiki_rootpath,sysid) - tmp = t.expectedfile.replace(os.path.dirname(builddir),'') - wikiname_e = wikify_filename(tmp,wiki_rootpath,sysid) - if hasattr(t, 'expectedfile_data'): - imgs[wikiname_e] = t.expectedfile_data - if t.actualfile: - actualfile_wiki = '[[File:'+wikiname_a+'|250px]]' - if hasattr(t, 'actualfile_data'): - imgs[wikiname_a] = t.actualfile_data - else: - actualfile_wiki = 'No image generated.' - newchunk = re.sub('FTESTNAME',t.fullname,repeat1) - newchunk = newchunk.replace('ACTUALFILE_WIKI',actualfile_wiki) - newchunk = newchunk.replace('EXPECTEDFILE',wikiname_e) - newchunk = newchunk.replace('TESTLOG',t.fulltestlog) - s = s.replace(repeat1, newchunk+repeat1) - else: - raise Exception("Unknown test type %r"%t.type) - - makefiles_wikinames = {} - for mf in sorted(makefiles.keys()): - tmp = mf.replace('CMakeFiles','').replace('.dir','') - wikiname = wikify_filename(tmp,wiki_rootpath,sysid) - newchunk = re.sub('MAKEFILE_NAME',wikiname,repeat3) - s = s.replace(repeat3, newchunk+repeat3) - makefiles_wikinames[mf] = wikiname - - s = s.replace(repeat1,'') - s = s.replace(repeat2,'') - s = s.replace(repeat3,'') - s = re.sub('\n','',s) - s = re.sub('','',s) - - mainpage_wikiname = wiki_rootpath + '_' + sysid + '_test_report' - txtpages[ mainpage_wikiname ] = s - for mf in sorted(makefiles.keys()): - txtpages[ makefiles_wikinames[ mf ] ] = '\n*Subreport from [['+mainpage_wikiname+']]\n\n\n
    \n'+makefiles[mf]+'\n
    ' - - return imgs, txtpages - -def png_encode64( fname, width=250 ): - # en.wikipedia.org/wiki/Data_URI_scheme - try: - f = open( fname, "rb" ) - data = f.read() - except: - data = '' - data_uri = data.encode("base64").replace("\n","") - tag = '' - tail = '' - - passed_tests = filter(lambda x: x.passed, tests) - failed_tests = filter(lambda x: not x.passed, tests) - try: percent = str(int(100.0*len(passed_tests) / len(tests))) - except ZeroDivisionError: percent = 'n/a' - - tests_to_report = failed_tests - if include_passed: tests_to_report = tests - - s='' - - s+= '\n

    ' - s+= '\nSystem info\n' - s+= '\n

    ' - s+= '

    '+sysinfo+'
    \n' - - s+= '\n
    '
    -	s+= '\nSTARTDATE: '+ startdate
    -	s+= '\nENDDATE: '+ enddate
    -	s+= '\nWIKI_ROOTPATH: '+ wiki_rootpath
    -	s+= '\nSYSID: '+sysid
    -	s+= '\nNUMTESTS: '+str(len(tests))
    -	s+= '\nNUMPASSED: '+str(len(passed_tests))
    -	s+= '\nPERCENTPASSED: '+ percent
    -	s+= '\n
    ' - - if not include_passed: - s+= '

    Failed tests:

    \n' - - if len(tests_to_report)==0: - s+= '

    none

    ' - - for t in tests_to_report: - if t.type in ('txt', 'ast', 'csg', 'term', 'echo'): - s+='\n
    '+t.fullname+'
    \n' - s+='

    '+t.fulltestlog+'
    \n\n' - elif t.type=='png': - tmp = t.actualfile.replace(builddir,'') - wikiname_a = wikify_filename(tmp,wiki_rootpath,sysid) - tmp = t.expectedfile.replace(os.path.dirname(builddir),'') - wikiname_e = wikify_filename(tmp,wiki_rootpath,sysid) - # imgtag_e = ' - # imatag_a = ' - imgtag_e = png_encode64( t.expectedfile, 250 ) - imgtag_a = png_encode64( t.actualfile, 250 ) - s+='' - s+='\n' - s+='\n ' - s+='\n
    '+t.fullname - s+='\n
    ExpectedActual' - s+='\n
    ' + imgtag_e + '' + imgtag_a + '
    ' - s+='\n
    '
    -			s+=t.fulltestlog
    -			s+='\n
    ' - else: - raise Exception("Unknown test type %r"%t.type) - - s+='\n\n

    \n\n' - - s+= '

    CMake .build files

    \n' - s+= '\n
    '
    -	makefiles_wikinames = {}
    -	for mf in sorted(makefiles.keys()):
    -		mfname = mf.strip().lstrip(os.path.sep)
    -		text = open(os.path.join(builddir,mfname)).read()
    -		s+= '

    '+mfname+'

    '
    -		s+= text
    -		tmp = mf.replace('CMakeFiles','').replace('.dir','')
    -		wikiname = wikify_filename(tmp,wiki_rootpath,sysid)
    -		# s += '\n'+wikiname+'
    ' - s+= '\n
    ' - s+='\n' - - return head + s + tail - -def wiki_login(wikiurl,api_php_path,botname,botpass): - site = mwclient.Site(wikiurl,api_php_path) - site.login(botname,botpass) - return site - -def wiki_upload(wikiurl,api_php_path,botname,botpass,filedata,wikipgname): - counter = 0 - done = False - descrip = 'test' - time.sleep(1) - while not done: - try: - print 'login',botname,'to',wikiurl - site = wiki_login(wikiurl,api_php_path,botname,botpass) - print 'uploading...', - if wikipgname.endswith('png'): - site.upload(filedata,wikipgname,descrip,ignore=True) - else: - page = site.Pages[wikipgname] - text = page.edit() - page.save(filedata) - done = True - print 'transfer ok' - except Exception, e: - print 'Error:', type(e),e - counter += 1 - if counter>maxretry: - print 'giving up. please try a different wiki site' - done = True - else: - print 'wiki',wikiurl,'down. retrying in 15 seconds' - time.sleep(15) - -def upload(wikiurl,api_php_path='/',wiki_rootpath='test', sysid='null', botname='cakebaby',botpass='anniew',wikidir='.',dryrun=True): - wetrun = not dryrun - if dryrun: print 'dry run' - try: - global mwclient - import mwclient - except: - print 'please download mwclient 0.6.5 and unpack here:', os.getcwd() - sys.exit() - - if wetrun: site = wiki_login(wikiurl,api_php_path,botname,botpass) - - wikifiles = os.listdir(wikidir) - testreport_page = filter( lambda x: 'test_report' in x, wikifiles ) - if (len(testreport_page)>1): - print 'multiple test reports found, please clean dir',wikidir - sys.exit() - - rootpage = testreport_page[0] - print 'add',rootpage,' to main report page ',wiki_rootpath - if wetrun: - page = site.Pages[wiki_rootpath] - text = page.edit() - if not '[['+rootpage+']]' in text: - page.save(text +'\n*[['+rootpage+']]\n') - - wikifiles = os.listdir(wikidir) - wikifiles = filter(lambda x: not x.endswith('html'), wikifiles) - - print 'upload wiki pages:' - for wikiname in wikifiles: - filename = os.path.join(wikidir,wikiname) - filedata = tryread(filename) - print 'upload',len(filedata),'bytes from',wikiname - if wetrun and len(filedata)>0: - wiki_upload(wikiurl,api_php_path,botname,botpass,filedata,wikiname) - if len(filedata)==0: - print 'cancelling empty upload' def findlogfile(builddir): - logpath = os.path.join(builddir,'Testing','Temporary') - logfilename = os.path.join(logpath,'LastTest.log.tmp') - if not os.path.isfile(logfilename): - logfilename = os.path.join(logpath,'LastTest.log') - if not os.path.isfile(logfilename): - print 'cant find and/or open logfile',logfilename - sys.exit() - return logpath, logfilename + logpath = os.path.join(builddir, 'Testing', 'Temporary') + logfilename = os.path.join(logpath, 'LastTest.log.tmp') + if not os.path.isfile(logfilename): + logfilename = os.path.join(logpath, 'LastTest.log') + if not os.path.isfile(logfilename): + print 'can\'t find and/or open logfile', logfilename + sys.exit() + return logfilename + +# --- Templating --- + +class Templates(object): + html_template = ''' + Test run for {sysid} + {style} + + +

    {project_name} test run report

    +

    + Sysid: {sysid} +

    +

    + Result summary: {numpassed} / {numtests} tests passed ({percent}%) +

    + +

    System info

    +
    {sysinfo}
    + +

    start time: {startdate}

    +

    end time: {enddate}

    + +

    Image tests

    + {image_tests} + +

    Text tests

    + {text_tests} + +

    build.make and flags.make

    + {makefiles} + ''' + + style = ''' + ''' + + image_template = ''' + + + + + +
    {test_name}
    Expected image Actual image
    {expected} {actual}
    + +
    +    {test_log}
    +    
    + ''' + + text_template = ''' + {test_name} + +
    +    {test_log}
    +    
    + ''' + + makefile_template = ''' +

    {name}

    +
    +        {text}
    +    
    + ''' + + def __init__(self, **defaults): + self.filled = {} + self.defaults = defaults + + def fill(self, template, *args, **kwargs): + kwds = self.defaults.copy() + kwds.update(kwargs) + return getattr(self, template).format(*args, **kwds) + + def add(self, template, var, *args, **kwargs): + self.filled[var] = self.filled.get(var, '') + self.fill(template, *args, **kwargs) + return self.filled[var] + + def get(self, var): + return self.filled[var] + + +def to_html(project_name, startdate, tests, enddate, sysinfo, sysid, makefiles): + passed_tests = [test for test in tests if test.passed] + failed_tests = [test for test in tests if not test.passed] + + report_tests = failed_tests + if include_passed: + report_tests = tests + + percent = '%.0f' % (100.0 * len(passed_tests) / len(tests)) if tests else 'n/a' + + templates = Templates() + for test in report_tests: + if test.type in ('txt', 'ast', 'csg', 'term', 'echo'): + templates.add('text_template', 'text_tests', + test_name=test.fullname, + test_log=test.fulltestlog) + elif test.type == 'png': + actual_img = png_encode64(test.actualfile, + data=vars(test).get('actualfile_data')) + expected_img = png_encode64(test.expectedfile, + data=vars(test).get('expectedfile_data')) + templates.add('image_template', 'image_tests', + test_name=test.fullname, + test_log=test.fulltestlog, + actual=actual_img, + expected=expected_img) + else: + raise TypeError('Unknown test type %r' % test.type) + + for mf in sorted(makefiles.keys()): + mfname = mf.strip().lstrip(os.path.sep) + text = open(os.path.join(builddir, mfname)).read() + templates.add('makefile_template', 'makefiles', name=mfname, text=text) + + text_tests = templates.get('text_tests') + image_tests = templates.get('image_tests') + makefiles_str = templates.get('makefiles') + + return templates.fill('html_template', style=Templates.style, + sysid=sysid, sysinfo=sysinfo, + startdate=startdate, enddate=enddate, + project_name=project_name, + numtests=len(tests), + numpassed=len(passed_tests), + percent=percent, image_tests=image_tests, + text_tests=text_tests, makefiles=makefiles_str) + +# --- End Templating --- + +# --- Web Upload --- + +def postify(data): + return urlencode(data).encode() + +def create_page(): + data = { + 'action': 'create', + 'type': 'html' + } + try: + response = urlopen('http://www.dinkypage.com', data=postify(data)) + except: + return None + return response.geturl() + +def upload_html(page_url, title, html): + data = { + 'mode': 'editor', + 'title': title, + 'html': html, + 'ajax': '1' + } + try: + response = urlopen(page_url, data=postify(data)) + except: + return False + return 'success' in response.read().decode() + +# --- End Web Upload --- def debug(x): - if debug_test_pp: print 'test_pretty_print: '+x + if debug_test_pp: + print 'test_pretty_print: ' + x -debug_test_pp=False -builddir=os.getcwd() +debug_test_pp = False +builddir = os.getcwd() def main(): - #wikisite = 'cakebaby.referata.com' - #wiki_api_path = '' - global wikisite, wiki_api_path, wiki_rootpath, builddir, debug_test_pp - global maxretry, dry, include_passed + global builddir, debug_test_pp + global maxretry, dry, include_passed + project_name = 'OpenSCAD' + + if bool(os.getenv("TEST_GENERATE")): + sys.exit(0) + + # --- Command Line Parsing --- + + if '--debug' in ' '.join(sys.argv): + debug_test_pp = True + maxretry = 10 - wikisite = 'cakebaby.wikia.com' - wiki_api_path = '/' - wiki_rootpath = 'OpenSCAD' - if '--debug' in string.join(sys.argv): debug_test_pp=True - maxretry = 10 + include_passed = False + if '--include-passed' in sys.argv: + include_passed = True - if bool(os.getenv("TEST_GENERATE")): sys.exit(0) + dry = False + debug('running test_pretty_print') + if '--dryrun' in sys.argv: + dry = True - include_passed = False - if '--include-passed' in sys.argv: include_passed = True + suffix = ezsearch('--suffix=(.*?) ', ' '.join(sys.argv) + ' ') + builddir = ezsearch('--builddir=(.*?) ', ' '.join(sys.argv) + ' ') + if not builddir: + builddir = os.getcwd() + debug('build dir set to ' + builddir) + + # --- End Command Line Parsing --- - dry = False - debug( 'running test_pretty_print' ) - if '--dryrun' in sys.argv: dry=True - suffix = ezsearch('--suffix=(.*?) ',string.join(sys.argv)+' ') - builddir = ezsearch('--builddir=(.*?) ',string.join(sys.argv)+' ') - if builddir=='': builddir=os.getcwd() - debug( 'build dir set to ' + builddir ) + sysinfo, sysid = read_sysinfo(os.path.join(builddir, 'sysinfo.txt')) + makefiles = load_makefiles(builddir) + logfilename = findlogfile(builddir) + testlog = tryread(logfilename) + startdate, tests, enddate = parselog(testlog) + if debug_test_pp: + print 'found sysinfo.txt,', + print 'found', len(makefiles),'makefiles,', + print 'found', len(tests),'test results' - sysinfo, sysid = read_sysinfo(os.path.join(builddir,'sysinfo.txt')) - makefiles = load_makefiles(builddir) - logpath, logfilename = findlogfile(builddir) - testlog = tryread(logfilename) - startdate, tests, enddate = parselog(testlog) - if debug_test_pp: - print 'found sysinfo.txt,', - print 'found', len(makefiles),'makefiles,', - print 'found', len(tests),'test results' + html = to_html(project_name, startdate, tests, enddate, sysinfo, sysid, makefiles) + html_basename = sysid + '_report.html' + html_filename = os.path.join(builddir, 'Testing', 'Temporary', html_basename) + debug('saving ' + html_filename + ' ' + str(len(htmldata)) + ' bytes') + trysave(html_filename, html) - imgs, txtpages = towiki(wiki_rootpath, startdate, tests, enddate, sysinfo, sysid, makefiles) + page_url = create_page() + if upload_html(page_url, title='OpenSCAD test results', html=html): + share_url = page_url.partition('?')[0] + print 'html report uploaded at', share_url + else: + print 'could not upload html report' - wikidir = os.path.join(logpath,sysid+'_report') - debug( 'erasing files in ' + wikidir ) - try: map(lambda x:os.remove(os.path.join(wikidir,x)), os.listdir(wikidir)) - except: pass - debug( 'output dir:\n' + wikidir.replace(os.getcwd(),'') ) - debug( 'writing ' + str(len(imgs)) + ' images' ) - debug( 'writing ' + str(len(txtpages)-1) + ' text pages' ) - debug( 'writing index.html ' ) - if '--wiki' in string.join(sys.argv): - print "wiki output is deprecated" - for pgname in sorted(imgs): trysave( os.path.join(wikidir,pgname), imgs[pgname]) - for pgname in sorted(txtpages): trysave( os.path.join(wikidir,pgname), txtpages[pgname]) - - htmldata = tohtml(wiki_rootpath, startdate, tests, enddate, sysinfo, sysid, makefiles) - html_basename = sysid+'_report.html' - html_filename = os.path.join(builddir,'Testing','Temporary',html_basename) - debug('saving ' +html_filename + ' ' + str(len(htmldata)) + ' bytes') - trysave( html_filename, htmldata ) - print "report saved:", html_filename.replace(os.getcwd()+os.path.sep,'') - - if '--wiki-upload' in sys.argv: - print "wiki upload is deprecated." - upload(wikisite,wiki_api_path,wiki_rootpath,sysid,'openscadbot', - 'tobdacsnepo',wikidir,dryrun=dry) - print 'upload attempt complete' - - debug( 'test_pretty_print complete' ) + debug('test_pretty_print complete') if __name__=='__main__': - main() + main() From 8d3365a79ab7afd287545da3f8b2ba12f0c43585 Mon Sep 17 00:00:00 2001 From: a-e-m Date: Sat, 7 Dec 2013 14:50:49 -0800 Subject: [PATCH 31/66] Update design philosopy comments --- tests/test_pretty_print.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/test_pretty_print.py b/tests/test_pretty_print.py index c26b919d..44df156f 100755 --- a/tests/test_pretty_print.py +++ b/tests/test_pretty_print.py @@ -17,11 +17,9 @@ # Design philosophy # # 1. parse the data (images, logs) into easy-to-use data structures -# 2. wikifiy the data -# 3. save the wikified data to disk -# 4. generate html, including base64 encoding of images -# 5. save html file -# 6. upload html to public site and share with others +# 2. generate html, including base64 encoding of images +# 3. save html file +# 4. upload html to public site and share with others # todo # From 8a21092dc01dfe54550b8fceada35999bed3056b Mon Sep 17 00:00:00 2001 From: a-e-m Date: Sat, 7 Dec 2013 17:59:07 -0800 Subject: [PATCH 32/66] Adding imports --- tests/test_pretty_print.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_pretty_print.py b/tests/test_pretty_print.py index 44df156f..c2deab8c 100755 --- a/tests/test_pretty_print.py +++ b/tests/test_pretty_print.py @@ -33,6 +33,13 @@ import hashlib import subprocess import time import platform +try: + from urllib.request import urlopen + from urllib.parse import urlencode +except ImportError: + from urllib2 import urlopen + from urllib import urlencode + def tryread(filename): data = None From b131464f954b2d10f6b065b45038652af2379742 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Sun, 8 Dec 2013 13:50:03 -0500 Subject: [PATCH 33/66] #559 CMAKE_OSX_DEPLOYMENT_TARGET needs to be cached --- tests/CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b84775b8..f92eddf5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -14,14 +14,14 @@ include(CMakeParseArguments.cmake) # Detect Lion and force gcc IF (APPLE) - # Somehow, since we build dependencies for 10.7, we need to also build executables - # for 10.7. This used to not be necessary, but since 10.9 it apparently is.. - SET(CMAKE_OSX_DEPLOYMENT_TARGET 10.7) EXECUTE_PROCESS(COMMAND sw_vers -productVersion OUTPUT_VARIABLE MACOSX_VERSION) IF (NOT ${MACOSX_VERSION} VERSION_LESS "10.9.0") message("Detected Maverick (10.9) or later") set(CMAKE_C_COMPILER "clang") set(CMAKE_CXX_COMPILER "clang++") + # Somehow, since we build dependencies for 10.7, we need to also build executables + # for 10.7. This used to not be necessary, but since 10.9 it apparently is.. + SET(CMAKE_OSX_DEPLOYMENT_TARGET 10.7 CACHE STRING "Deployment target") ELSEIF (NOT ${MACOSX_VERSION} VERSION_LESS "10.8.0") message("Detected Mountain Lion (10.8)") set(CMAKE_C_COMPILER "clang") From cb2b094b269646c79f48f525306b74cd7bc0f633 Mon Sep 17 00:00:00 2001 From: a-e-m Date: Sun, 8 Dec 2013 10:51:25 -0800 Subject: [PATCH 34/66] Fixed small error in Templates.get --- tests/test_pretty_print.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_pretty_print.py b/tests/test_pretty_print.py index c2deab8c..349a730a 100755 --- a/tests/test_pretty_print.py +++ b/tests/test_pretty_print.py @@ -304,10 +304,10 @@ class Templates(object): def add(self, template, var, *args, **kwargs): self.filled[var] = self.filled.get(var, '') + self.fill(template, *args, **kwargs) - return self.filled[var] + return self.get(var) def get(self, var): - return self.filled[var] + return self.filled.get(var, '') def to_html(project_name, startdate, tests, enddate, sysinfo, sysid, makefiles): From 8971c67fa209fe29ead54034cd0f9be39c0909ff Mon Sep 17 00:00:00 2001 From: a-e-m Date: Sun, 8 Dec 2013 11:18:58 -0800 Subject: [PATCH 35/66] Fixed variable name error --- tests/test_pretty_print.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pretty_print.py b/tests/test_pretty_print.py index 349a730a..359165e8 100755 --- a/tests/test_pretty_print.py +++ b/tests/test_pretty_print.py @@ -441,7 +441,7 @@ def main(): html = to_html(project_name, startdate, tests, enddate, sysinfo, sysid, makefiles) html_basename = sysid + '_report.html' html_filename = os.path.join(builddir, 'Testing', 'Temporary', html_basename) - debug('saving ' + html_filename + ' ' + str(len(htmldata)) + ' bytes') + debug('saving ' + html_filename + ' ' + str(len(html)) + ' bytes') trysave(html_filename, html) page_url = create_page() From a22ebd608d941b5ae1767a2903f4bacc925840ed Mon Sep 17 00:00:00 2001 From: a-e-m Date: Sun, 8 Dec 2013 11:28:19 -0800 Subject: [PATCH 36/66] Moved include_passed --- tests/test_pretty_print.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pretty_print.py b/tests/test_pretty_print.py index 359165e8..c3035c55 100755 --- a/tests/test_pretty_print.py +++ b/tests/test_pretty_print.py @@ -395,6 +395,7 @@ def debug(x): print 'test_pretty_print: ' + x debug_test_pp = False +include_passed = False builddir = os.getcwd() def main(): @@ -411,7 +412,6 @@ def main(): debug_test_pp = True maxretry = 10 - include_passed = False if '--include-passed' in sys.argv: include_passed = True From e27f971d9c3dc26a9a023a2aea4c265dac53437b Mon Sep 17 00:00:00 2001 From: a-e-m Date: Sun, 8 Dec 2013 12:15:04 -0800 Subject: [PATCH 37/66] Changed upload code to use gists --- tests/test_pretty_print.py | 90 +++++++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 36 deletions(-) diff --git a/tests/test_pretty_print.py b/tests/test_pretty_print.py index c3035c55..a88a22ea 100755 --- a/tests/test_pretty_print.py +++ b/tests/test_pretty_print.py @@ -17,9 +17,11 @@ # Design philosophy # # 1. parse the data (images, logs) into easy-to-use data structures -# 2. generate html, including base64 encoding of images -# 3. save html file -# 4. upload html to public site and share with others +# 2. wikifiy the data +# 3. save the wikified data to disk +# 4. generate html, including base64 encoding of images +# 5. save html file +# 6. upload html to public site and share with others # todo # @@ -34,12 +36,12 @@ import subprocess import time import platform try: - from urllib.request import urlopen - from urllib.parse import urlencode -except ImportError: - from urllib2 import urlopen - from urllib import urlencode - + from urllib.request import urlopen, Request +except: + from urllib2 import urlopen, Request +import json +import base64 + def tryread(filename): data = None @@ -304,7 +306,7 @@ class Templates(object): def add(self, template, var, *args, **kwargs): self.filled[var] = self.filled.get(var, '') + self.fill(template, *args, **kwargs) - return self.get(var) + return self.filled[var] def get(self, var): return self.filled.get(var, '') @@ -361,32 +363,46 @@ def to_html(project_name, startdate, tests, enddate, sysinfo, sysid, makefiles): # --- Web Upload --- -def postify(data): - return urlencode(data).encode() +API_URL = 'https://api.github.com/%s' +# Username is personal access token, from https://github.com/settings/applications +# This way, no password is needed +USERNAME = 'b2af28787fb1efd9a5b3a3b4f1be8a3ac9b5b335' +PASSWORD = '' -def create_page(): - data = { - 'action': 'create', - 'type': 'html' - } +def make_auth(username, password): + auth = '%s:%s' % (USERNAME, PASSWORD) + return base64.b64encode(auth.encode()) + +def post_gist(name, content): + gist = '''{ + "description": "", + "public": true, + "files": { + "%s": { + "content": "%s" + } + } + }''' + gist = gist % (name, content) + + req = Request(API_URL % 'gists') + req.add_header('Authorization', b'Basic ' + make_auth(USERNAME, PASSWORD)) try: - response = urlopen('http://www.dinkypage.com', data=postify(data)) + result = urlopen(req, data=gist) except: + print 'Could not upload results' return None - return response.geturl() + return json.loads(result.read()) -def upload_html(page_url, title, html): - data = { - 'mode': 'editor', - 'title': title, - 'html': html, - 'ajax': '1' - } - try: - response = urlopen(page_url, data=postify(data)) - except: - return False - return 'success' in response.read().decode() + +def get_raw_urls(result): + files = result.get('files', {}) + for file in files: + yield files[file].get('raw_url').replace('gist.github.com', 'rawgithub.com') + +result = post_gist('aaabbb.html', '''

    I\'m asdf

    ''') +for url in get_raw_urls(result): + print(url) # --- End Web Upload --- @@ -438,18 +454,20 @@ def main(): print 'found', len(makefiles),'makefiles,', print 'found', len(tests),'test results' + html = to_html(project_name, startdate, tests, enddate, sysinfo, sysid, makefiles) html_basename = sysid + '_report.html' html_filename = os.path.join(builddir, 'Testing', 'Temporary', html_basename) debug('saving ' + html_filename + ' ' + str(len(html)) + ' bytes') trysave(html_filename, html) - page_url = create_page() - if upload_html(page_url, title='OpenSCAD test results', html=html): - share_url = page_url.partition('?')[0] - print 'html report uploaded at', share_url - else: + result = post_gist(name=html_basename, content=html) + if result is None: print 'could not upload html report' + return + + for url in get_raw_urls(result): + print 'html report uploaded at', url debug('test_pretty_print complete') From 88cc7edafd47ec167609c05eb22c3f8eb642d225 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Sun, 8 Dec 2013 15:16:58 -0500 Subject: [PATCH 38/66] #559 Fix Qt font rendering on OS X 10.9 --- patches/qt4/patch-qeventdispatcher.diff | 86 +++++++++++++++++++++++++ patches/qt4/patch-qfontdatabase.diff | 29 +++++++++ 2 files changed, 115 insertions(+) create mode 100644 patches/qt4/patch-qeventdispatcher.diff create mode 100644 patches/qt4/patch-qfontdatabase.diff diff --git a/patches/qt4/patch-qeventdispatcher.diff b/patches/qt4/patch-qeventdispatcher.diff new file mode 100644 index 00000000..89ed4788 --- /dev/null +++ b/patches/qt4/patch-qeventdispatcher.diff @@ -0,0 +1,86 @@ +--- src/gui/kernel/qeventdispatcher_mac_p.h 2013-06-07 01:16:59.000000000 -0400 ++++ src/gui/kernel/qeventdispatcher_mac_p_new-8184b49c12d887928921ed5b695c8c6f04a07514.h 2013-12-08 14:31:01.000000000 -0500 +@@ -173,6 +173,7 @@ + #ifdef QT_MAC_USE_COCOA + // The following variables help organizing modal sessions: + static QStack cocoaModalSessionStack; ++ static QStack cocoaModalSessionStackPendingEnd; + static bool currentExecIsNSAppRun; + static bool nsAppRunCalledByQt; + static bool cleanupModalSessionsNeeded; +@@ -180,6 +181,7 @@ + static NSModalSession currentModalSession(); + static void updateChildrenWorksWhenModal(); + static void temporarilyStopAllModalSessions(); ++ static void stopAllPendingEndModalSessions(); + static void beginModalSession(QWidget *widget); + static void endModalSession(QWidget *widget); + static void cancelWaitForMoreEvents(); +--- src/gui/kernel/qeventdispatcher_mac.mm 2013-06-07 01:16:59.000000000 -0400 ++++ src/gui/kernel/qeventdispatcher_mac_new-833e02de99494686f8dd7a567f6e19e847508f11.mm 2013-12-08 14:30:59.000000000 -0500 +@@ -603,6 +603,9 @@ + while ([NSApp runModalSession:session] == NSRunContinuesResponse && !d->interrupt) + qt_mac_waitForMoreModalSessionEvents(); + ++ // stop all pending end modal sessions ++ d->stopAllPendingEndModalSessions(); ++ + if (!d->interrupt && session == d->currentModalSessionCached) { + // Someone called [NSApp stopModal:] from outside the event + // dispatcher (e.g to stop a native dialog). But that call wrongly stopped +@@ -678,6 +681,9 @@ + if (!d->interrupt) + QCoreApplicationPrivate::sendPostedEvents(0, 0, d->threadData); + ++ // stop all pending end modal sessions ++ d->stopAllPendingEndModalSessions(); ++ + // Since the window that holds modality might have changed while processing + // events, we we need to interrupt when we return back the previous process + // event recursion to ensure that we spin the correct modal session. +@@ -781,6 +787,7 @@ + + #ifdef QT_MAC_USE_COCOA + QStack QEventDispatcherMacPrivate::cocoaModalSessionStack; ++QStack QEventDispatcherMacPrivate::cocoaModalSessionStackPendingEnd; + bool QEventDispatcherMacPrivate::currentExecIsNSAppRun = false; + bool QEventDispatcherMacPrivate::nsAppRunCalledByQt = false; + bool QEventDispatcherMacPrivate::cleanupModalSessionsNeeded = false; +@@ -828,6 +835,20 @@ + currentModalSessionCached = 0; + } + ++void QEventDispatcherMacPrivate::stopAllPendingEndModalSessions() ++{ ++ // stop all modal sessions pending end ++ int stackSize = cocoaModalSessionStackPendingEnd.size(); ++ for (int i=stackSize-1; i>=0; --i) { ++ QCocoaModalSessionInfo &info = cocoaModalSessionStackPendingEnd[i]; ++ cocoaModalSessionStackPendingEnd.remove(i); ++ if (info.session) { ++ [NSApp endModalSession:info.session]; ++ [(NSWindow *)info.nswindow release]; ++ } ++ } ++} ++ + NSModalSession QEventDispatcherMacPrivate::currentModalSession() + { + // If we have one or more modal windows, this function will create +@@ -925,10 +946,12 @@ + } + cocoaModalSessionStack.remove(i); + currentModalSessionCached = 0; +- if (info.session) { +- [NSApp endModalSession:info.session]; +- [(NSWindow *)info.nswindow release]; +- } ++ ++ // Cannot stop the sessions here since we might still be inside a ++ // [NSApp runModalSession:] call. Add the session to the pending end stack and ++ // process the stack after the call to [NSApp runModalSession:] returns. ++ if (info.session) ++ cocoaModalSessionStackPendingEnd.push(info); + } + + updateChildrenWorksWhenModal(); diff --git a/patches/qt4/patch-qfontdatabase.diff b/patches/qt4/patch-qfontdatabase.diff new file mode 100644 index 00000000..c0788906 --- /dev/null +++ b/patches/qt4/patch-qfontdatabase.diff @@ -0,0 +1,29 @@ +--- src/gui/text/qfontdatabase.cpp 2013-06-07 01:16:59.000000000 -0400 ++++ src/gui/text/qfontdatabase_new-bb2beddc3ae55c4676d190d0ac99aa32d322a6a5.cpp 2013-12-08 14:51:10.000000000 -0500 +@@ -441,6 +441,7 @@ + #endif + #if !defined(QWS) && defined(Q_OS_MAC) + bool fixedPitchComputed : 1; ++ QString postscriptName; + #endif + #ifdef Q_WS_X11 + bool symbol_checked : 1; +--- src/gui/text/qfontdatabase_mac.cpp 2013-06-07 01:16:59.000000000 -0400 ++++ src/gui/text/qfontdatabase_mac_new-41f29865db84152efb41c048470f713353a0a84c.cpp 2013-12-08 14:51:05.000000000 -0500 +@@ -147,6 +147,7 @@ + QCFString family_name = (CFStringRef)CTFontDescriptorCopyLocalizedAttribute(font, kCTFontFamilyNameAttribute, NULL); + QCFString style_name = (CFStringRef)CTFontDescriptorCopyLocalizedAttribute(font, kCTFontStyleNameAttribute, NULL); + QtFontFamily *family = db->family(family_name, true); ++ family->postscriptName = QCFString((CFStringRef)CTFontDescriptorCopyAttribute(font, kCTFontNameAttribute)); + + if (QCFType languages = (CFArrayRef) CTFontDescriptorCopyAttribute(font, kCTFontLanguagesAttribute)) { + CFIndex length = CFArrayGetCount(languages); +@@ -327,7 +328,7 @@ + if (db->families[k]->name.compare(family_list.at(i), Qt::CaseInsensitive) == 0) { + QByteArray family_name = db->families[k]->name.toUtf8(); + #if defined(QT_MAC_USE_COCOA) +- QCFType ctFont = CTFontCreateWithName(QCFString(db->families[k]->name), 12, NULL); ++ QCFType ctFont = CTFontCreateWithName(QCFString(db->families[k]->postscriptName), 12, NULL); + if (ctFont) { + fontName = CTFontCopyFullName(ctFont); + goto found; From eb046015d2fd4fa3e4e3c8844cd6dc8f4d3eca99 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Sun, 8 Dec 2013 15:17:17 -0500 Subject: [PATCH 39/66] #559 Fix Qt font rendering on OS X 10.9 --- src/openscad.cc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/openscad.cc b/src/openscad.cc index ece68189..ab842357 100644 --- a/src/openscad.cc +++ b/src/openscad.cc @@ -474,6 +474,13 @@ bool QtUseGUI() int gui(vector &inputFiles, const fs::path &original_path, int argc, char ** argv) { +#ifdef Q_OS_MACX + if (QSysInfo::MacintoshVersion > QSysInfo::MV_10_8) { + // fix Mac OS X 10.9 (mavericks) font issue + // https://bugreports.qt-project.org/browse/QTBUG-32789 + QFont::insertSubstitution(".Lucida Grande UI", "Lucida Grande"); + } +#endif QApplication app(argc, argv, true); //useGUI); #ifdef Q_WS_MAC app.installEventFilter(new EventFilter(&app)); From d6bffc4691cf1467cf93d527724f7278b418273d Mon Sep 17 00:00:00 2001 From: a-e-m Date: Sun, 8 Dec 2013 12:17:19 -0800 Subject: [PATCH 40/66] Taking out test accidentially left in, my user token --- tests/test_pretty_print.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_pretty_print.py b/tests/test_pretty_print.py index a88a22ea..b601c842 100755 --- a/tests/test_pretty_print.py +++ b/tests/test_pretty_print.py @@ -366,7 +366,7 @@ def to_html(project_name, startdate, tests, enddate, sysinfo, sysid, makefiles): API_URL = 'https://api.github.com/%s' # Username is personal access token, from https://github.com/settings/applications # This way, no password is needed -USERNAME = 'b2af28787fb1efd9a5b3a3b4f1be8a3ac9b5b335' +USERNAME = '' # add OpenScad user token PASSWORD = '' def make_auth(username, password): @@ -400,9 +400,6 @@ def get_raw_urls(result): for file in files: yield files[file].get('raw_url').replace('gist.github.com', 'rawgithub.com') -result = post_gist('aaabbb.html', '''

    I\'m asdf

    ''') -for url in get_raw_urls(result): - print(url) # --- End Web Upload --- From 7075d8d9c4dde62798022bdfe02c5d57e997ba43 Mon Sep 17 00:00:00 2001 From: Brody Kenrick Date: Thu, 5 Dec 2013 15:56:50 +1100 Subject: [PATCH 41/66] Fix for bad boost libraries Get this error because of a search for a non-existent library on linux64 ----------- [ 69%] Built target tests-cgal Scanning dependencies of target cgalcachetest [ 70%] Building CXX object CMakeFiles/cgalcachetest.dir/cgalcachetest.cc.o make[2]: *** No rule to make target `/usr/lib/libboost_thread.so', needed by `cgalcachetest'. Stop. make[1]: *** [CMakeFiles/cgalcachetest.dir/all] Error 2 make: *** [all] Error 2 [2]+ Done gedit openscad.pro (wd: ~/git/openscad_unicode) ---------- --- tests/CMakeLists.txt | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f92eddf5..d7ad18a4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -384,11 +384,45 @@ if("${CGAL_MAJOR_VERSION}.${CGAL_MINOR_VERSION}" VERSION_LESS 3.6) endif() inclusion(CGAL_DIR CGAL_INCLUDE_DIRS) +#Get rid of bad libraries suggested for BOOST dependencies (they don't exist on some machines and cause build failures). +#/usr/lib/libboost_thread.so;/usr/lib/libboost_system.so; +string(FIND "${CGAL_3RD_PARTY_LIBRARIES}" "/usr/lib/libboost_system.so" FIND_POSITION ) +if(NOT "-1" STREQUAL ${FIND_POSITION} ) +if(NOT EXISTS "/usr/lib/libboost_system.so") + MESSAGE( WARNING "CGAL_3RD_PARTY_LIBRARIES:Found erroneous /usr/lib/libboost_system.so -- stripping" ) + string(REPLACE "/usr/lib/libboost_system.so" "" CGAL_3RD_PARTY_LIBRARIES ${CGAL_3RD_PARTY_LIBRARIES}) +endif() +endif() +string(FIND "${CGAL_3RD_PARTY_LIBRARIES}" "/usr/lib/libboost_thread.so" FIND_POSITION ) +if(NOT "-1" STREQUAL ${FIND_POSITION} ) +if(NOT EXISTS "/usr/lib/libboost_thread.so") + MESSAGE( WARNING "CGAL_3RD_PARTY_LIBRARIES:Found erroneous /usr/lib/libboost_thread.so -- stripping" ) + string(REPLACE "/usr/lib/libboost_thread.so" "" CGAL_3RD_PARTY_LIBRARIES ${CGAL_3RD_PARTY_LIBRARIES}) +endif() +endif() if(${CMAKE_CXX_COMPILER} MATCHES ".*clang.*" AND NOT ${CGAL_CXX_FLAGS_INIT} STREQUAL "" ) string(REPLACE "-frounding-math" "" CGAL_CXX_FLAGS_INIT ${CGAL_CXX_FLAGS_INIT}) string(REPLACE "--param=ssp-buffer-size=4" "" CGAL_CXX_FLAGS_INIT ${CGAL_CXX_FLAGS_INIT}) endif() +if (NOT $ENV{OPENSCAD_LIBRARIES} STREQUAL "") + # Force pkg-config to look _only_ in the local library folder + # in case OPENSCAD_LIBRARIES is set. + set(ENV{PKG_CONFIG_PATH} "$ENV{OPENSCAD_LIBRARIES}/lib/pkgconfig") + set(ENV{PKG_CONFIG_LIBDIR} "$ENV{OPENSCAD_LIBRARIES}/lib/pkgconfig") +endif() + +# Find libraries (system installed or dependency built) using pkg-config +find_package(PkgConfig REQUIRED) + +#GLib-2 +pkg_search_module(GLIB2 REQUIRED glib-2.0>=2.2.0) +#Can't use the CXXFlags directly as they are ;-separated +string(REPLACE ";" " " GLIB2_CFLAGS "${GLIB2_CFLAGS}") +message(STATUS "glib-2.0 found: ${GLIB2_VERSION}") + +add_definitions(${GLIB2_CFLAGS}) + # Imagemagick if (SKIP_IMAGEMAGICK) From 3abf64249fd667f0b7f558ecfbbf35cfe9916a5d Mon Sep 17 00:00:00 2001 From: Brody Kenrick Date: Thu, 5 Dec 2013 17:56:54 +1100 Subject: [PATCH 42/66] Unicode support for strings Add suport for using unicode strings in .scad files. Support iterating across them/accessing them via [] and searching. -------- Add GLIB (to build for test and normal build -- both with installed and built locally development files). Add support for unicode chars to length and search builtin functions and [] for strings. Added unicode testing functions. Ad GLIB to library info page. --- .gitignore | 1 + README.md | 1 + common.pri | 1 + glib-2.0.pri | 38 ++++++ openscad.pro | 3 +- scripts/check-dependencies.sh | 17 ++- scripts/uni-build-dependencies.sh | 1 + scripts/uni-get-dependencies.sh | 15 +-- src/AboutDialog.html | 1 + src/PlatformUtils.cc | 3 + src/func.cc | 49 ++++++-- src/value.cc | 18 ++- testdata/scad/misc/search-tests-unicode.scad | 116 ++++++++++++++++++ testdata/scad/misc/string-unicode.scad | 36 ++++++ tests/CMakeLists.txt | 6 +- .../search-tests-unicode-expected.echo | 109 ++++++++++++++++ .../echotest/string-unicode-expected.echo | 104 ++++++++++++++++ 17 files changed, 497 insertions(+), 22 deletions(-) create mode 100644 glib-2.0.pri create mode 100644 testdata/scad/misc/search-tests-unicode.scad create mode 100644 testdata/scad/misc/string-unicode.scad create mode 100644 tests/regression/echotest/search-tests-unicode-expected.echo create mode 100644 tests/regression/echotest/string-unicode-expected.echo diff --git a/.gitignore b/.gitignore index 50dace12..59bac496 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /*.scad *.dmg +*~ *.tar* Makefile objects diff --git a/README.md b/README.md index 27f12cec..1e97e0f6 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ Follow the instructions for the platform you're compiling on below. * [OpenCSG (1.3.2)](http://www.opencsg.org/) * [GLEW (1.5.4 ->)](http://glew.sourceforge.net/) * [Eigen (3.0 - 3.2)](http://eigen.tuxfamily.org/) +* [glib2 (2.2.0)](https://developer.gnome.org/glib/) * [GCC C++ Compiler (4.2 ->)](http://gcc.gnu.org/) * [Bison (2.4)](http://www.gnu.org/software/bison/) * [Flex (2.5.35)](http://flex.sourceforge.net/) diff --git a/common.pri b/common.pri index 7153ded7..696c8b1d 100644 --- a/common.pri +++ b/common.pri @@ -11,3 +11,4 @@ include(opencsg.pri) include(glew.pri) include(eigen.pri) include(boost.pri) +include(glib-2.0.pri) \ No newline at end of file diff --git a/glib-2.0.pri b/glib-2.0.pri new file mode 100644 index 00000000..0fbc4e2d --- /dev/null +++ b/glib-2.0.pri @@ -0,0 +1,38 @@ +# Detect glib-2.0, then use this priority list to determine +# which library to use: +# +# Priority +# 1. GLIB2_INCLUDEPATH / GLIB2_LIBPATH (qmake parameter, not checked it given on commandline) +# 2. OPENSCAD_LIBRARIES (environment variable) +# 3. system's standard include paths from pkg-config + +glib-2.0 { + +# read environment variables +OPENSCAD_LIBRARIES_DIR = $$(OPENSCAD_LIBRARIES) +GLIB2_DIR = $$(GLIB2DIR) + +!isEmpty(OPENSCAD_LIBRARIES_DIR) { + isEmpty(GLIB2_INCLUDEPATH) { + GLIB2_INCLUDEPATH_1 = $$OPENSCAD_LIBRARIES_DIR/include/glib-2.0 + GLIB2_INCLUDEPATH_2 = $$OPENSCAD_LIBRARIES_DIR/lib/glib-2.0/include + GLIB2_LIBPATH = $$OPENSCAD_LIBRARIES_DIR/lib + } +} + +isEmpty(GLIB2_INCLUDEPATH) { + GLIB2_CFLAGS = $$system("pkg-config --cflags glib-2.0") +} else { + GLIB2_CFLAGS = -I$$GLIB2_INCLUDEPATH_1 + GLIB2_CFLAGS += -I$$GLIB2_INCLUDEPATH_2 +} + +isEmpty(GLIB2_LIBPATH) { + GLIB2_LIBS = $$system("pkg-config --libs glib-2.0") +} else { + GLIB2_LIBS = -L$$GLIB2_LIBPATH -lglib-2.0 +} + +QMAKE_CXXFLAGS += $$GLIB2_CFLAGS +LIBS += $$GLIB2_LIBS +} diff --git a/openscad.pro b/openscad.pro index b38419ef..ec5af20c 100644 --- a/openscad.pro +++ b/openscad.pro @@ -8,7 +8,7 @@ # OPENCSGDIR # OPENSCAD_LIBRARIES # -# Please see the 'Buildling' sections of the OpenSCAD user manual +# Please see the 'Building' sections of the OpenSCAD user manual # for updated tips & workarounds. # # http://en.wikibooks.org/wiki/OpenSCAD_User_Manual @@ -156,6 +156,7 @@ CONFIG += cgal CONFIG += opencsg CONFIG += boost CONFIG += eigen +CONFIG += glib-2.0 #Uncomment the following line to enable QCodeEdit #CONFIG += qcodeedit diff --git a/scripts/check-dependencies.sh b/scripts/check-dependencies.sh index b63c6770..e5871989 100755 --- a/scripts/check-dependencies.sh +++ b/scripts/check-dependencies.sh @@ -66,6 +66,21 @@ cgal_sysver() cgal_sysver_result=`grep "define *CGAL_VERSION *[0-9.]*" $cgalpath | awk '{print $3}'` } +glib2_sysver() +{ + #Get architecture triplet - e.g. x86_64-linux-gnu + glib2archtriplet=`gcc -dumpmachine 2>/dev/null` + if [ -z "$VAR" ]; then + glib2archtriplet=`dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null` + fi + glib2path=$1/lib/$glib2archtriplet/glib-2.0/include/glibconfig.h + if [ ! -e $glib2path ]; then return; fi + glib2major=`grep "define *GLIB_MAJOR_VERSION *[0-9.]*" $glib2path | awk '{print $3}'` + glib2minor=`grep "define *GLIB_MINOR_VERSION *[0-9.]*" $glib2path | awk '{print $3}'` + glib2micro=`grep "define *GLIB_MICRO_VERSION *[0-9.]*" $glib2path | awk '{print $3}'` + glib2_sysver_result="${glib2major}.${glib2minor}.${glib2micro}" +} + boost_sysver() { boostpath=$1/include/boost/version.hpp @@ -530,7 +545,7 @@ checkargs() main() { - deps="qt4 cgal gmp mpfr boost opencsg glew eigen gcc bison flex make" + deps="qt4 cgal gmp mpfr boost opencsg glew eigen glib2 gcc bison flex make" #deps="$deps curl git" # not technically necessary for build #deps="$deps python cmake imagemagick" # only needed for tests #deps="cgal" diff --git a/scripts/uni-build-dependencies.sh b/scripts/uni-build-dependencies.sh index e652c473..8d912c35 100755 --- a/scripts/uni-build-dependencies.sh +++ b/scripts/uni-build-dependencies.sh @@ -603,5 +603,6 @@ build_boost 1.53.0 build_cgal 4.0.2 build_glew 1.9.0 build_opencsg 1.3.2 +build_glib2 2.38.2 echo "OpenSCAD dependencies built and installed to " $BASEDIR diff --git a/scripts/uni-get-dependencies.sh b/scripts/uni-get-dependencies.sh index a0306ef8..d2408c00 100755 --- a/scripts/uni-get-dependencies.sh +++ b/scripts/uni-get-dependencies.sh @@ -8,7 +8,7 @@ get_fedora_deps() { sudo yum install qt-devel bison flex eigen3-devel python-paramiko \ boost-devel mpfr-devel gmp-devel glew-devel CGAL-devel gcc gcc-c++ pkgconfig \ - opencsg-devel git libXmu-devel curl imagemagick ImageMagick make \ + opencsg-devel git libXmu-devel curl imagemagick ImageMagick glib2-devel make \ xorg-x11-server-Xvfb } @@ -21,7 +21,7 @@ get_altlinux_deps() { for i in boost-devel boost-filesystem-devel gcc4.5 gcc4.5-c++ boost-program_options-devel \ boost-thread-devel boost-system-devel boost-regex-devel eigen3 libmpfr libgmp libgmp_cxx-devel qt4-devel libcgal-devel git-core \ - libglew-devel flex bison curl imagemagick; do sudo apt-get install $i; done + libglew-devel flex bison curl imagemagick glib2-devel; do sudo apt-get install $i; done } get_freebsd_deps() @@ -29,20 +29,21 @@ get_freebsd_deps() pkg_add -r bison boost-libs cmake git bash eigen3 flex gmake gmp mpfr \ xorg libGLU libXmu libXi xorg-vfbserver glew \ qt4-corelib qt4-gui qt4-moc qt4-opengl qt4-qmake qt4-rcc qt4-uic \ - opencsg cgal curl imagemagick + opencsg cgal curl imagemagick glib2-devel } get_netbsd_deps() { sudo pkgin install bison boost cmake git bash eigen flex gmake gmp mpfr \ qt4 glew cgal opencsg modular-xorg python27 py27-paramiko curl \ - imagemagick ImageMagick + imagemagick ImageMagick glib2-devel } get_opensuse_deps() { sudo zypper install libeigen3-devel mpfr-devel gmp-devel boost-devel \ - libqt4-devel glew-devel cmake git bison flex cgal-devel opencsg-devel curl + libqt4-devel glew-devel cmake git bison flex cgal-devel opencsg-devel curl \ + glib2-devel } get_mageia_deps() @@ -50,7 +51,7 @@ get_mageia_deps() sudo urpmi ctags sudo urpmi task-c-devel task-c++-devel libqt4-devel libgmp-devel \ libmpfr-devel libboost-devel eigen3-devel libglew-devel bison flex \ - cmake imagemagick python curl git x11-server-xvfb + cmake imagemagick glib2-devel python curl git x11-server-xvfb } get_debian_deps() @@ -59,7 +60,7 @@ get_debian_deps() libxmu-dev cmake bison flex git-core libboost-all-dev \ libXi-dev libmpfr-dev libboost-dev libglew-dev \ libeigen3-dev libcgal-dev libopencsg-dev libgmp3-dev libgmp-dev \ - python-paramiko curl imagemagick; do + python-paramiko curl imagemagick libglib2.0-dev; do sudo apt-get -y install $pkg; done } diff --git a/src/AboutDialog.html b/src/AboutDialog.html index 99e7c3b2..65a54d71 100644 --- a/src/AboutDialog.html +++ b/src/AboutDialog.html @@ -64,6 +64,7 @@ Please visit this link for a copy of the license: C++, GCC, clang
  • python
  • Nullsoft installer +
  • GLib

    diff --git a/src/PlatformUtils.cc b/src/PlatformUtils.cc index b02b822e..8b39f6df 100644 --- a/src/PlatformUtils.cc +++ b/src/PlatformUtils.cc @@ -1,6 +1,8 @@ #include "PlatformUtils.h" #include "boosty.h" +#include + bool PlatformUtils::createLibraryPath() { std::string path = PlatformUtils::libraryPath(); @@ -114,6 +116,7 @@ std::string PlatformUtils::info() << "\nOpenCSG version: " << OPENCSG_VERSION_STRING << "\nQt version: " << qtVersion << "\nMingW build: " << mingwstatus + << "\nGLib version: " << GLIB_MAJOR_VERSION << "." << GLIB_MINOR_VERSION << "." << GLIB_MICRO_VERSION << "\nOPENSCADPATH: " << getenv("OPENSCADPATH") << "\n" ; return s.str(); diff --git a/src/func.cc b/src/func.cc index 865a2b42..4587f725 100644 --- a/src/func.cc +++ b/src/func.cc @@ -45,6 +45,8 @@ #include #include +/*Unicode support for string lengths and array accesses*/ +#include #ifdef __WIN32__ #include @@ -306,7 +308,11 @@ Value builtin_length(const Context *, const EvalContext *evalctx) { if (evalctx->numArgs() == 1) { if (evalctx->getArgValue(0).type() == Value::VECTOR) return Value(int(evalctx->getArgValue(0).toVector().size())); - if (evalctx->getArgValue(0).type() == Value::STRING) return Value(int(evalctx->getArgValue(0).toString().size())); + if (evalctx->getArgValue(0).type() == Value::STRING) { + //Unicode glyph count for the length -- rather than the string (num. of bytes) length. + std::string text = evalctx->getArgValue(0).toString(); + return Value(int( g_utf8_strlen( text.c_str(), text.size() ) )); + } } return Value(); } @@ -380,10 +386,17 @@ Value builtin_lookup(const Context *, const EvalContext *evalctx) num_returns_per_match : int; index_col_num : int; + The search string and searched strings can be unicode strings. Examples: Index values return as list: search("a","abcdabcd"); - - returns [0,4] + - returns [0] + search("Л","Л"); //A unicode string + - returns [0] + search("🂡aЛ","a🂡Л🂡a🂡Л🂡a",0); + - returns [[1,3,5,7],[0,4,8],[2,6]] + search("a","abcdabcd",0); //Search up to all matches + - returns [[0,4]] search("a","abcdabcd",1); - returns [0] search("e","abcdabcd",1); @@ -433,16 +446,25 @@ Value builtin_search(const Context *, const EvalContext *evalctx) } } else if (findThis.type() == Value::STRING) { unsigned int searchTableSize; - if (searchTable.type() == Value::STRING) searchTableSize = searchTable.toString().size(); - else searchTableSize = searchTable.toVector().size(); - for (size_t i = 0; i < findThis.toString().size(); i++) { + //Unicode glyph count for the length + unsigned int findThisSize = g_utf8_strlen( findThis.toString().c_str(), findThis.toString().size() ); + if (searchTable.type() == Value::STRING) { + searchTableSize = g_utf8_strlen( searchTable.toString().c_str(), searchTable.toString().size() ); + } else { + searchTableSize = searchTable.toVector().size(); + } + for (size_t i = 0; i < findThisSize; i++) { unsigned int matchCount = 0; Value::VectorType resultvec; for (size_t j = 0; j < searchTableSize; j++) { - if ((searchTable.type() == Value::VECTOR && - findThis.toString()[i] == searchTable.toVector()[j].toVector()[index_col_num].toString()[0]) || - (searchTable.type() == Value::STRING && - findThis.toString()[i] == searchTable.toString()[j])) { + gchar* ptr_ft = g_utf8_offset_to_pointer(findThis.toString().c_str(), i); + gchar* ptr_st = NULL; + if(searchTable.type() == Value::VECTOR) { + ptr_st = g_utf8_offset_to_pointer(searchTable.toVector()[j].toVector()[index_col_num].toString().c_str(), 0); + } else if(searchTable.type() == Value::STRING){ + ptr_st = g_utf8_offset_to_pointer(searchTable.toString().c_str(), j); + } + if( (ptr_ft) && (ptr_st) && (g_utf8_get_char(ptr_ft) == g_utf8_get_char(ptr_st)) ) { Value resultValue((double(j))); matchCount++; if (num_returns_per_match == 1) { @@ -454,7 +476,14 @@ Value builtin_search(const Context *, const EvalContext *evalctx) if (num_returns_per_match > 1 && matchCount >= num_returns_per_match) break; } } - if (matchCount == 0) PRINTB(" WARNING: search term not found: \"%s\"", findThis.toString()[i]); + if (matchCount == 0) { + gchar* ptr_ft = g_utf8_offset_to_pointer(findThis.toString().c_str(), i); + gchar utf8_of_cp[6] = ""; //A buffer for a single unicode character to be copied into + if(ptr_ft) { + g_utf8_strncpy( utf8_of_cp, ptr_ft, 1 ); + } + PRINTB(" WARNING: search term not found: \"%s\"", utf8_of_cp ); + } if (num_returns_per_match == 0 || num_returns_per_match > 1) { returnvec.push_back(Value(resultvec)); } diff --git a/src/value.cc b/src/value.cc index 5afb650c..c8a88c69 100644 --- a/src/value.cc +++ b/src/value.cc @@ -36,6 +36,8 @@ #include #include "boost-utils.h" #include "boosty.h" +/*Unicode support for string lengths and array accesses*/ +#include std::ostream &operator<<(std::ostream &stream, const Filename &filename) { @@ -579,14 +581,28 @@ Value Value::operator-() const } */ +/* + * bracket operation [] detecting multi-byte unicode. + * If the string is multi-byte unicode then the index will offset to the character (2 or 4 byte) and not to the byte. + * A 'normal' string with byte chars are a subset of unicode and still work. + */ class bracket_visitor : public boost::static_visitor { public: Value operator()(const std::string &str, const double &idx) const { int i = int(idx); Value v; + //Check that the index is positive and less than the size in bytes if ((i >= 0) && (i < (int)str.size())) { - v = Value(str[int(idx)]); + //Ensure character (not byte) index is inside the character/glyph array + if( (unsigned) i < g_utf8_strlen( str.c_str(), str.size() ) ) { + gchar utf8_of_cp[6] = ""; //A buffer for a single unicode character to be copied into + gchar* ptr = g_utf8_offset_to_pointer(str.c_str(), i); + if(ptr) { + g_utf8_strncpy(utf8_of_cp, ptr, 1); + } + v = std::string(utf8_of_cp); + } // std::cout << "bracket_visitor: " << v << "\n"; } return v; diff --git a/testdata/scad/misc/search-tests-unicode.scad b/testdata/scad/misc/search-tests-unicode.scad new file mode 100644 index 00000000..d863eff9 --- /dev/null +++ b/testdata/scad/misc/search-tests-unicode.scad @@ -0,0 +1,116 @@ +//Test search with unicode strings + +//Helper function that pretty prints our search test +//Expected result is checked against execution of a search() invocation and OK/FAIL is indicated +module test_search_and_echo( exp_res, search_to_find, search_to_search, search_up_to_num_matches = undef) +{ + if(undef != search_up_to_num_matches) + { + assign( test_res = search(search_to_find, search_to_search, search_up_to_num_matches) ) + echo(str("Expect ", exp_res, " for search(", search_to_find, ", ", search_to_search, ", ", search_up_to_num_matches, ")=", test_res, ". ", (exp_res == test_res)?"OK":"FAIL" )); + } + else + { + assign( test_res = search(search_to_find, search_to_search) ) + echo(str("Expect ", exp_res, " for search(", search_to_find, ", ", search_to_search, ")=", test_res, ". ", (exp_res == test_res)?"OK":"FAIL" )); + } +} + + +//"Normal" text for comparison +echo ("----- Lookup of 1 byte into 1 byte"); +//Hits - up_to_count 1 +test_search_and_echo( [0], "a","aaaa" ); +test_search_and_echo( [0], "a","aaaa",1 ); +test_search_and_echo( [0,0], "aa","aaaa" ); +test_search_and_echo( [0,0], "aa","aaaa",1 ); + + +//Hits - up to count 1+ (incl 0 == all) +test_search_and_echo( [[0,1,2,3]] , "a","aaaa",0 ); +test_search_and_echo( [[0,1]], "a","aaaa",2 ); +test_search_and_echo( [[0,1,2]], "a","aaaa",3 ); +test_search_and_echo( [[0,1,2,3]] , "a","aaaa",4 ); +test_search_and_echo( [[0,1,2,3],[0,1,2,3]] , "aa","aaaa",0 ); +//Misses +test_search_and_echo( [], "b","aaaa" ); +test_search_and_echo( [], "b","aaaa",1 ); +test_search_and_echo( [[]], "b","aaaa",0 ); +test_search_and_echo( [[]], "b","aaaa",2 ); + +test_search_and_echo( [], "bb","aaaa" ); +test_search_and_echo( [], "bb","aaaa",1 ); +test_search_and_echo( [[],[]], "bb","aaaa",0 ); +test_search_and_echo( [[],[]], "bb","aaaa",2 ); +//Miss - empties +test_search_and_echo( [], "","aaaa" ); +test_search_and_echo( [], "","" ); +test_search_and_echo( [], "a","" ); + + +//Unicode tests +echo ("----- Lookup of multi-byte into 1 byte"); +test_search_and_echo( [], "Л","aaaa" ); +test_search_and_echo( [], "🂡","aaaa" ); +test_search_and_echo( [[]], "Л","aaaa",0 ); +test_search_and_echo( [[]], "🂡","aaaa",0 ); + +test_search_and_echo( [], "ЛЛ","aaaa" ); +test_search_and_echo( [], "🂡🂡","aaaa" ); +test_search_and_echo( [[],[]], "ЛЛ","aaaa",0 ); +test_search_and_echo( [[],[]], "🂡🂡","aaaa",0 ); + +echo ("----- Lookup of 1-byte into multi-byte"); +test_search_and_echo( [] , "a","ЛЛЛЛ" ); +test_search_and_echo( [] , "a","🂡🂡🂡🂡" ); +test_search_and_echo( [] , "a","ЛЛЛЛ",1 ); + +test_search_and_echo( [[]] , "a","🂡🂡🂡🂡",0 ); +test_search_and_echo( [[]] , "a","🂡🂡🂡🂡",2 ); + +echo ("----- Lookup of 1-byte into mixed multi-byte"); +test_search_and_echo( [0], "a","aЛaЛaЛaЛa" ); +test_search_and_echo( [0], "a","a🂡a🂡a🂡a🂡a" ); +test_search_and_echo( [0], "a","a🂡Л🂡a🂡Л🂡a" ); + +test_search_and_echo( [[0,2,4,6,8]], "a","aЛaЛaЛaЛa",0 ); +test_search_and_echo( [[0,2,4,6,8]], "a","a🂡a🂡a🂡a🂡a", 0 ); +test_search_and_echo( [[0,4,8]] , "a","a🂡Л🂡a🂡Л🂡a", 0 ); + +echo ("----- Lookup of 2-byte into 2-byte"); +test_search_and_echo( [0] , "Л","ЛЛЛЛ" ); +test_search_and_echo( [[0,1,2,3]] , "Л","ЛЛЛЛ",0 ); + +echo ("----- Lookup of 2-byte into 4-byte"); +test_search_and_echo( [] , "Л","🂡🂡🂡🂡" ); + +echo ("----- Lookup of 4-byte into 4-byte"); +test_search_and_echo( [0] , "🂡","🂡🂡🂡🂡" ); +test_search_and_echo( [[0,1,2,3]], "🂡","🂡🂡🂡🂡",0 ); + +echo ("----- Lookup of 4-byte into 2-byte"); +test_search_and_echo( [] , "🂡","ЛЛЛЛ" ); + +echo ("----- Lookup of 2-byte into mixed multi-byte"); +test_search_and_echo( [1] , "Л","aЛaЛaЛaЛa",1 ); +test_search_and_echo( [] , "Л","a🂡a🂡a🂡a🂡a", 1 ); +test_search_and_echo( [2] , "Л","a🂡Л🂡a🂡Л🂡a", 1 ); + +test_search_and_echo( [[1,3,5,7]] , "Л","aЛaЛaЛaЛa",0 ); +test_search_and_echo( [[]] , "Л","a🂡a🂡a🂡a🂡a", 0 ); +test_search_and_echo( [[2,6]] , "Л","a🂡Л🂡a🂡Л🂡a", 0 ); + +echo ("----- Lookup of 4-byte into mixed multi-byte"); +test_search_and_echo( [] , "🂡","aЛaЛaЛaЛa",1 ); +test_search_and_echo( [1] , "🂡","a🂡a🂡a🂡a🂡a", 1 ); + +test_search_and_echo( [[]] , "🂡","aЛaЛaЛaЛa",0 ); +test_search_and_echo( [[1,3,5,7]] , "🂡","a🂡a🂡a🂡a🂡a", 0 ); +test_search_and_echo( [[1,3,5,7]] , "🂡","a🂡Л🂡a🂡Л🂡a", 0 ); + +echo ("----- Lookup of mixed multi-byte into mixed multi-byte"); +test_search_and_echo( [[0,2,4,6,8],[1,3,5,7],[]], "aЛ🂡","aЛaЛaЛaЛa",0 ); +test_search_and_echo( [[0,2,4,6,8],[],[1,3,5,7]], "aЛ🂡","a🂡a🂡a🂡a🂡a", 0 ); +test_search_and_echo( [[0,4,8],[2,6],[1,3,5,7]] , "aЛ🂡","a🂡Л🂡a🂡Л🂡a", 0 ); +test_search_and_echo( [[1,3,5,7],[0,4,8],[2,6]] , "🂡aЛ","a🂡Л🂡a🂡Л🂡a", 0 ); + diff --git a/testdata/scad/misc/string-unicode.scad b/testdata/scad/misc/string-unicode.scad new file mode 100644 index 00000000..d8e3e5c9 --- /dev/null +++ b/testdata/scad/misc/string-unicode.scad @@ -0,0 +1,36 @@ +//Test how well arrays of unicode string are accessed. + +texts_array = [ +"DEADBEEF", +"Ленивый рыжий кот", +"كسول الزنجبيل القط", +"懶惰的姜貓", +"äöü ÄÖÜ ß", +"😁😂😃😄😅😆😇😈😉😊😋😌😍😎😏😐", +"⠁⠂⠃⠄⠅⠆⠇⠈⠉⠊⠋⠌⠍⠎⠏", +"🂡🂱🃁🃑", +]; + +text_2bytes = "Ленивый рыжий кот"; +text_4bytes = "🂡🂱🃁🃑"; + + +//Test all the normal accesses +for (text_array_idx = [0:(len(texts_array)-1)]) +{ + echo( "[", text_array_idx, "] = ", texts_array[text_array_idx], " of len=", len(texts_array[text_array_idx]), ":" ); + for (text_idx = [0:(len(texts_array[text_array_idx])-1)]) + { + echo( " [", text_idx, ,"]=", texts_array[text_array_idx][text_idx] ); + } +} + +//Test one past the last element of (x-byte unicode). This will be one past the length but inside the char length of the string +echo( "Past end of unicode only 2-byte ", text_2bytes[len(text_2bytes)] ); +echo( "Past end of unicode only 4-byte ", text_4bytes[len(text_4bytes)] ); + +//Test past the last element of (x-byte unicode). Outside both lengths. +echo( "Past end of both 2-byte ", text_2bytes[ len(text_2bytes) * 2 ] ); +echo( "Past end of both 4-byte ", text_4bytes[ len(text_4bytes) * 4 ] ); + + diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d7ad18a4..3c19b77c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -594,8 +594,8 @@ set(OFFSCREEN_SOURCES ../src/OpenCSGRenderer.cc) add_library(tests-core STATIC ${CORE_SOURCES}) -target_link_libraries(tests-core ${OPENGL_LIBRARIES}) -set(TESTS-CORE-LIBRARIES ${OPENGL_LIBRARIES} ${Boost_LIBRARIES}) +target_link_libraries(tests-core ${OPENGL_LIBRARIES} ${GLIB2_LIBRARIES} ) +set(TESTS-CORE-LIBRARIES ${OPENGL_LIBRARIES} ${GLIB2_LIBRARIES} ${Boost_LIBRARIES} ) add_library(tests-common STATIC ${COMMON_SOURCES}) target_link_libraries(tests-common tests-core) @@ -815,8 +815,10 @@ list(APPEND ECHO_FILES ${FUNCTION_FILES} ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/dim-all.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/string-test.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/string-indexing.scad + ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/string-unicode.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/vector-values.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/search-tests.scad + ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/search-tests-unicode.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/recursion-tests.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/value-reassignment-tests.scad ${CMAKE_SOURCE_DIR}/../testdata/scad/misc/value-reassignment-tests2.scad diff --git a/tests/regression/echotest/search-tests-unicode-expected.echo b/tests/regression/echotest/search-tests-unicode-expected.echo new file mode 100644 index 00000000..801bc8c8 --- /dev/null +++ b/tests/regression/echotest/search-tests-unicode-expected.echo @@ -0,0 +1,109 @@ +ECHO: "----- Lookup of 1 byte into 1 byte" +ECHO: "Expect [0] for search(a, aaaa)=[0]. OK" +ECHO: "Expect [0] for search(a, aaaa, 1)=[0]. OK" +ECHO: "Expect [0, 0] for search(aa, aaaa)=[0, 0]. OK" +ECHO: "Expect [0, 0] for search(aa, aaaa, 1)=[0, 0]. OK" +ECHO: "Expect [[0, 1, 2, 3]] for search(a, aaaa, 0)=[[0, 1, 2, 3]]. OK" +ECHO: "Expect [[0, 1]] for search(a, aaaa, 2)=[[0, 1]]. OK" +ECHO: "Expect [[0, 1, 2]] for search(a, aaaa, 3)=[[0, 1, 2]]. OK" +ECHO: "Expect [[0, 1, 2, 3]] for search(a, aaaa, 4)=[[0, 1, 2, 3]]. OK" +ECHO: "Expect [[0, 1, 2, 3], [0, 1, 2, 3]] for search(aa, aaaa, 0)=[[0, 1, 2, 3], [0, 1, 2, 3]]. OK" + WARNING: search term not found: "b" +ECHO: "Expect [] for search(b, aaaa)=[]. OK" + WARNING: search term not found: "b" +ECHO: "Expect [] for search(b, aaaa, 1)=[]. OK" + WARNING: search term not found: "b" +ECHO: "Expect [[]] for search(b, aaaa, 0)=[[]]. OK" + WARNING: search term not found: "b" +ECHO: "Expect [[]] for search(b, aaaa, 2)=[[]]. OK" + WARNING: search term not found: "b" + WARNING: search term not found: "b" +ECHO: "Expect [] for search(bb, aaaa)=[]. OK" + WARNING: search term not found: "b" + WARNING: search term not found: "b" +ECHO: "Expect [] for search(bb, aaaa, 1)=[]. OK" + WARNING: search term not found: "b" + WARNING: search term not found: "b" +ECHO: "Expect [[], []] for search(bb, aaaa, 0)=[[], []]. OK" + WARNING: search term not found: "b" + WARNING: search term not found: "b" +ECHO: "Expect [[], []] for search(bb, aaaa, 2)=[[], []]. OK" +ECHO: "Expect [] for search(, aaaa)=[]. OK" +ECHO: "Expect [] for search(, )=[]. OK" + WARNING: search term not found: "a" +ECHO: "Expect [] for search(a, )=[]. OK" +ECHO: "----- Lookup of multi-byte into 1 byte" + WARNING: search term not found: "Л" +ECHO: "Expect [] for search(Л, aaaa)=[]. OK" + WARNING: search term not found: "🂡" +ECHO: "Expect [] for search(🂡, aaaa)=[]. OK" + WARNING: search term not found: "Л" +ECHO: "Expect [[]] for search(Л, aaaa, 0)=[[]]. OK" + WARNING: search term not found: "🂡" +ECHO: "Expect [[]] for search(🂡, aaaa, 0)=[[]]. OK" + WARNING: search term not found: "Л" + WARNING: search term not found: "Л" +ECHO: "Expect [] for search(ЛЛ, aaaa)=[]. OK" + WARNING: search term not found: "🂡" + WARNING: search term not found: "🂡" +ECHO: "Expect [] for search(🂡🂡, aaaa)=[]. OK" + WARNING: search term not found: "Л" + WARNING: search term not found: "Л" +ECHO: "Expect [[], []] for search(ЛЛ, aaaa, 0)=[[], []]. OK" + WARNING: search term not found: "🂡" + WARNING: search term not found: "🂡" +ECHO: "Expect [[], []] for search(🂡🂡, aaaa, 0)=[[], []]. OK" +ECHO: "----- Lookup of 1-byte into multi-byte" + WARNING: search term not found: "a" +ECHO: "Expect [] for search(a, ЛЛЛЛ)=[]. OK" + WARNING: search term not found: "a" +ECHO: "Expect [] for search(a, 🂡🂡🂡🂡)=[]. OK" + WARNING: search term not found: "a" +ECHO: "Expect [] for search(a, ЛЛЛЛ, 1)=[]. OK" + WARNING: search term not found: "a" +ECHO: "Expect [[]] for search(a, 🂡🂡🂡🂡, 0)=[[]]. OK" + WARNING: search term not found: "a" +ECHO: "Expect [[]] for search(a, 🂡🂡🂡🂡, 2)=[[]]. OK" +ECHO: "----- Lookup of 1-byte into mixed multi-byte" +ECHO: "Expect [0] for search(a, aЛaЛaЛaЛa)=[0]. OK" +ECHO: "Expect [0] for search(a, a🂡a🂡a🂡a🂡a)=[0]. OK" +ECHO: "Expect [0] for search(a, a🂡Л🂡a🂡Л🂡a)=[0]. OK" +ECHO: "Expect [[0, 2, 4, 6, 8]] for search(a, aЛaЛaЛaЛa, 0)=[[0, 2, 4, 6, 8]]. OK" +ECHO: "Expect [[0, 2, 4, 6, 8]] for search(a, a🂡a🂡a🂡a🂡a, 0)=[[0, 2, 4, 6, 8]]. OK" +ECHO: "Expect [[0, 4, 8]] for search(a, a🂡Л🂡a🂡Л🂡a, 0)=[[0, 4, 8]]. OK" +ECHO: "----- Lookup of 2-byte into 2-byte" +ECHO: "Expect [0] for search(Л, ЛЛЛЛ)=[0]. OK" +ECHO: "Expect [[0, 1, 2, 3]] for search(Л, ЛЛЛЛ, 0)=[[0, 1, 2, 3]]. OK" +ECHO: "----- Lookup of 2-byte into 4-byte" + WARNING: search term not found: "Л" +ECHO: "Expect [] for search(Л, 🂡🂡🂡🂡)=[]. OK" +ECHO: "----- Lookup of 4-byte into 4-byte" +ECHO: "Expect [0] for search(🂡, 🂡🂡🂡🂡)=[0]. OK" +ECHO: "Expect [[0, 1, 2, 3]] for search(🂡, 🂡🂡🂡🂡, 0)=[[0, 1, 2, 3]]. OK" +ECHO: "----- Lookup of 4-byte into 2-byte" + WARNING: search term not found: "🂡" +ECHO: "Expect [] for search(🂡, ЛЛЛЛ)=[]. OK" +ECHO: "----- Lookup of 2-byte into mixed multi-byte" +ECHO: "Expect [1] for search(Л, aЛaЛaЛaЛa, 1)=[1]. OK" + WARNING: search term not found: "Л" +ECHO: "Expect [] for search(Л, a🂡a🂡a🂡a🂡a, 1)=[]. OK" +ECHO: "Expect [2] for search(Л, a🂡Л🂡a🂡Л🂡a, 1)=[2]. OK" +ECHO: "Expect [[1, 3, 5, 7]] for search(Л, aЛaЛaЛaЛa, 0)=[[1, 3, 5, 7]]. OK" + WARNING: search term not found: "Л" +ECHO: "Expect [[]] for search(Л, a🂡a🂡a🂡a🂡a, 0)=[[]]. OK" +ECHO: "Expect [[2, 6]] for search(Л, a🂡Л🂡a🂡Л🂡a, 0)=[[2, 6]]. OK" +ECHO: "----- Lookup of 4-byte into mixed multi-byte" + WARNING: search term not found: "🂡" +ECHO: "Expect [] for search(🂡, aЛaЛaЛaЛa, 1)=[]. OK" +ECHO: "Expect [1] for search(🂡, a🂡a🂡a🂡a🂡a, 1)=[1]. OK" + WARNING: search term not found: "🂡" +ECHO: "Expect [[]] for search(🂡, aЛaЛaЛaЛa, 0)=[[]]. OK" +ECHO: "Expect [[1, 3, 5, 7]] for search(🂡, a🂡a🂡a🂡a🂡a, 0)=[[1, 3, 5, 7]]. OK" +ECHO: "Expect [[1, 3, 5, 7]] for search(🂡, a🂡Л🂡a🂡Л🂡a, 0)=[[1, 3, 5, 7]]. OK" +ECHO: "----- Lookup of mixed multi-byte into mixed multi-byte" + WARNING: search term not found: "🂡" +ECHO: "Expect [[0, 2, 4, 6, 8], [1, 3, 5, 7], []] for search(aЛ🂡, aЛaЛaЛaЛa, 0)=[[0, 2, 4, 6, 8], [1, 3, 5, 7], []]. OK" + WARNING: search term not found: "Л" +ECHO: "Expect [[0, 2, 4, 6, 8], [], [1, 3, 5, 7]] for search(aЛ🂡, a🂡a🂡a🂡a🂡a, 0)=[[0, 2, 4, 6, 8], [], [1, 3, 5, 7]]. OK" +ECHO: "Expect [[0, 4, 8], [2, 6], [1, 3, 5, 7]] for search(aЛ🂡, a🂡Л🂡a🂡Л🂡a, 0)=[[0, 4, 8], [2, 6], [1, 3, 5, 7]]. OK" +ECHO: "Expect [[1, 3, 5, 7], [0, 4, 8], [2, 6]] for search(🂡aЛ, a🂡Л🂡a🂡Л🂡a, 0)=[[1, 3, 5, 7], [0, 4, 8], [2, 6]]. OK" diff --git a/tests/regression/echotest/string-unicode-expected.echo b/tests/regression/echotest/string-unicode-expected.echo new file mode 100644 index 00000000..b4b848fd --- /dev/null +++ b/tests/regression/echotest/string-unicode-expected.echo @@ -0,0 +1,104 @@ +ECHO: "[", 0, "] = ", "DEADBEEF", " of len=", 8, ":" +ECHO: " [", 0, "]=", "D" +ECHO: " [", 1, "]=", "E" +ECHO: " [", 2, "]=", "A" +ECHO: " [", 3, "]=", "D" +ECHO: " [", 4, "]=", "B" +ECHO: " [", 5, "]=", "E" +ECHO: " [", 6, "]=", "E" +ECHO: " [", 7, "]=", "F" +ECHO: "[", 1, "] = ", "Ленивый рыжий кот", " of len=", 17, ":" +ECHO: " [", 0, "]=", "Л" +ECHO: " [", 1, "]=", "е" +ECHO: " [", 2, "]=", "н" +ECHO: " [", 3, "]=", "и" +ECHO: " [", 4, "]=", "в" +ECHO: " [", 5, "]=", "ы" +ECHO: " [", 6, "]=", "й" +ECHO: " [", 7, "]=", " " +ECHO: " [", 8, "]=", "р" +ECHO: " [", 9, "]=", "ы" +ECHO: " [", 10, "]=", "ж" +ECHO: " [", 11, "]=", "и" +ECHO: " [", 12, "]=", "й" +ECHO: " [", 13, "]=", " " +ECHO: " [", 14, "]=", "к" +ECHO: " [", 15, "]=", "о" +ECHO: " [", 16, "]=", "т" +ECHO: "[", 2, "] = ", "كسول الزنجبيل القط", " of len=", 18, ":" +ECHO: " [", 0, "]=", "ك" +ECHO: " [", 1, "]=", "س" +ECHO: " [", 2, "]=", "و" +ECHO: " [", 3, "]=", "ل" +ECHO: " [", 4, "]=", " " +ECHO: " [", 5, "]=", "ا" +ECHO: " [", 6, "]=", "ل" +ECHO: " [", 7, "]=", "ز" +ECHO: " [", 8, "]=", "ن" +ECHO: " [", 9, "]=", "ج" +ECHO: " [", 10, "]=", "ب" +ECHO: " [", 11, "]=", "ي" +ECHO: " [", 12, "]=", "ل" +ECHO: " [", 13, "]=", " " +ECHO: " [", 14, "]=", "ا" +ECHO: " [", 15, "]=", "ل" +ECHO: " [", 16, "]=", "ق" +ECHO: " [", 17, "]=", "ط" +ECHO: "[", 3, "] = ", "懶惰的姜貓", " of len=", 5, ":" +ECHO: " [", 0, "]=", "懶" +ECHO: " [", 1, "]=", "惰" +ECHO: " [", 2, "]=", "的" +ECHO: " [", 3, "]=", "姜" +ECHO: " [", 4, "]=", "貓" +ECHO: "[", 4, "] = ", "äöü ÄÖÜ ß", " of len=", 9, ":" +ECHO: " [", 0, "]=", "ä" +ECHO: " [", 1, "]=", "ö" +ECHO: " [", 2, "]=", "ü" +ECHO: " [", 3, "]=", " " +ECHO: " [", 4, "]=", "Ä" +ECHO: " [", 5, "]=", "Ö" +ECHO: " [", 6, "]=", "Ü" +ECHO: " [", 7, "]=", " " +ECHO: " [", 8, "]=", "ß" +ECHO: "[", 5, "] = ", "😁😂😃😄😅😆😇😈😉😊😋😌😍😎😏😐", " of len=", 16, ":" +ECHO: " [", 0, "]=", "😁" +ECHO: " [", 1, "]=", "😂" +ECHO: " [", 2, "]=", "😃" +ECHO: " [", 3, "]=", "😄" +ECHO: " [", 4, "]=", "😅" +ECHO: " [", 5, "]=", "😆" +ECHO: " [", 6, "]=", "😇" +ECHO: " [", 7, "]=", "😈" +ECHO: " [", 8, "]=", "😉" +ECHO: " [", 9, "]=", "😊" +ECHO: " [", 10, "]=", "😋" +ECHO: " [", 11, "]=", "😌" +ECHO: " [", 12, "]=", "😍" +ECHO: " [", 13, "]=", "😎" +ECHO: " [", 14, "]=", "😏" +ECHO: " [", 15, "]=", "😐" +ECHO: "[", 6, "] = ", "⠁⠂⠃⠄⠅⠆⠇⠈⠉⠊⠋⠌⠍⠎⠏", " of len=", 15, ":" +ECHO: " [", 0, "]=", "⠁" +ECHO: " [", 1, "]=", "⠂" +ECHO: " [", 2, "]=", "⠃" +ECHO: " [", 3, "]=", "⠄" +ECHO: " [", 4, "]=", "⠅" +ECHO: " [", 5, "]=", "⠆" +ECHO: " [", 6, "]=", "⠇" +ECHO: " [", 7, "]=", "⠈" +ECHO: " [", 8, "]=", "⠉" +ECHO: " [", 9, "]=", "⠊" +ECHO: " [", 10, "]=", "⠋" +ECHO: " [", 11, "]=", "⠌" +ECHO: " [", 12, "]=", "⠍" +ECHO: " [", 13, "]=", "⠎" +ECHO: " [", 14, "]=", "⠏" +ECHO: "[", 7, "] = ", "🂡🂱🃁🃑", " of len=", 4, ":" +ECHO: " [", 0, "]=", "🂡" +ECHO: " [", 1, "]=", "🂱" +ECHO: " [", 2, "]=", "🃁" +ECHO: " [", 3, "]=", "🃑" +ECHO: "Past end of unicode only 2-byte ", undef +ECHO: "Past end of unicode only 4-byte ", undef +ECHO: "Past end of both 2-byte ", undef +ECHO: "Past end of both 4-byte ", undef From c0849eb1d3c98db505eec0396c56276a9a35120f Mon Sep 17 00:00:00 2001 From: Brody Kenrick Date: Fri, 6 Dec 2013 09:56:22 +1100 Subject: [PATCH 43/66] Update comments/messages for CGAL source of bad boost libraries Add comments and change to a status instead of a warning (as we recover nicely and it is a known issue in CGAL @ https://bugs.launchpad.net/ubuntu/+source/cgal/+bug/1242111) --- tests/CMakeLists.txt | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3c19b77c..2533103e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -383,23 +383,24 @@ if("${CGAL_MAJOR_VERSION}.${CGAL_MINOR_VERSION}" VERSION_LESS 3.6) message(FATAL_ERROR "CGAL >= 3.6 required") endif() inclusion(CGAL_DIR CGAL_INCLUDE_DIRS) - -#Get rid of bad libraries suggested for BOOST dependencies (they don't exist on some machines and cause build failures). -#/usr/lib/libboost_thread.so;/usr/lib/libboost_system.so; +#Remove bad BOOST libraries from CGAL 3rd party dependencies when they don't exist (such as on 64-bit Ubuntu 13.10). +#Libs of concern are /usr/lib/libboost_thread.so;/usr/lib/libboost_system.so; +#Confirmed bug in CGAL @ https://bugs.launchpad.net/ubuntu/+source/cgal/+bug/1242111 string(FIND "${CGAL_3RD_PARTY_LIBRARIES}" "/usr/lib/libboost_system.so" FIND_POSITION ) if(NOT "-1" STREQUAL ${FIND_POSITION} ) -if(NOT EXISTS "/usr/lib/libboost_system.so") - MESSAGE( WARNING "CGAL_3RD_PARTY_LIBRARIES:Found erroneous /usr/lib/libboost_system.so -- stripping" ) - string(REPLACE "/usr/lib/libboost_system.so" "" CGAL_3RD_PARTY_LIBRARIES ${CGAL_3RD_PARTY_LIBRARIES}) -endif() + if(NOT EXISTS "/usr/lib/libboost_system.so") + MESSAGE( STATUS "CGAL_3RD_PARTY_LIBRARIES:Removing non-existent /usr/lib/libboost_system.so" ) + string(REPLACE "/usr/lib/libboost_system.so" "" CGAL_3RD_PARTY_LIBRARIES ${CGAL_3RD_PARTY_LIBRARIES}) + endif() endif() string(FIND "${CGAL_3RD_PARTY_LIBRARIES}" "/usr/lib/libboost_thread.so" FIND_POSITION ) if(NOT "-1" STREQUAL ${FIND_POSITION} ) -if(NOT EXISTS "/usr/lib/libboost_thread.so") - MESSAGE( WARNING "CGAL_3RD_PARTY_LIBRARIES:Found erroneous /usr/lib/libboost_thread.so -- stripping" ) - string(REPLACE "/usr/lib/libboost_thread.so" "" CGAL_3RD_PARTY_LIBRARIES ${CGAL_3RD_PARTY_LIBRARIES}) -endif() + if(NOT EXISTS "/usr/lib/libboost_thread.so") + MESSAGE( STATUS "CGAL_3RD_PARTY_LIBRARIES:Removing non-existent /usr/lib/libboost_thread.so" ) + string(REPLACE "/usr/lib/libboost_thread.so" "" CGAL_3RD_PARTY_LIBRARIES ${CGAL_3RD_PARTY_LIBRARIES}) + endif() endif() + if(${CMAKE_CXX_COMPILER} MATCHES ".*clang.*" AND NOT ${CGAL_CXX_FLAGS_INIT} STREQUAL "" ) string(REPLACE "-frounding-math" "" CGAL_CXX_FLAGS_INIT ${CGAL_CXX_FLAGS_INIT}) string(REPLACE "--param=ssp-buffer-size=4" "" CGAL_CXX_FLAGS_INIT ${CGAL_CXX_FLAGS_INIT}) From d2575016b989b9cee5b44c29f10a76d84a7bf182 Mon Sep 17 00:00:00 2001 From: Brody Kenrick Date: Fri, 6 Dec 2013 17:46:52 +1100 Subject: [PATCH 44/66] Add in missed glib build dependencies for OS X and unix --- scripts/macosx-build-dependencies.sh | 25 +++++++++++++++++++++++++ scripts/uni-build-dependencies.sh | 25 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/scripts/macosx-build-dependencies.sh b/scripts/macosx-build-dependencies.sh index d4ca1f7c..5e26feee 100755 --- a/scripts/macosx-build-dependencies.sh +++ b/scripts/macosx-build-dependencies.sh @@ -285,6 +285,30 @@ build_glew() make GLEW_DEST=$DEPLOYDIR CC=$CC CFLAGS.EXTRA="-no-cpp-precomp -dynamic -fno-common -mmacosx-version-min=$MAC_OSX_VERSION_MIN $GLEW_EXTRA_FLAGS -arch x86_64" LDFLAGS.EXTRA="-mmacosx-version-min=$MAC_OSX_VERSION_MIN $GLEW_EXTRA_FLAGS -arch x86_64" STRIP= install } +build_glib2() +{ + version="$1" + maj_min_version="${version%.*}" #Drop micro + + if [ -e $DEPLOYDIR/lib/glib-2.0 ]; then + echo "glib2 already installed. not building" + return + fi + + echo "Building glib2 $version..." + cd "$BASEDIR"/src + rm -rf "glib-$version" + if [ ! -f "glib-$version.tar.xz" ]; then + curl --insecure -LO "http://ftp.gnome.org/pub/gnome/sources/glib/$maj_min_version/glib-$version.tar.xz" + fi + tar xJf "glib-$version.tar.xz" + cd "glib-$version" + + ./configure --prefix="$DEPLOYDIR" + make -j$NUMCPU + make install +} + build_opencsg() { version=$1 @@ -449,6 +473,7 @@ build_boost 1.54.0 # NB! For CGAL, also update the actual download URL in the function build_cgal 4.3 build_glew 1.10.0 +build_glib2 2.38.1 build_opencsg 1.3.2 if $OPTION_DEPLOY; then # build_sparkle andymatuschak 0ed83cf9f2eeb425d4fdd141c01a29d843970c20 diff --git a/scripts/uni-build-dependencies.sh b/scripts/uni-build-dependencies.sh index 8d912c35..ba328b7e 100755 --- a/scripts/uni-build-dependencies.sh +++ b/scripts/uni-build-dependencies.sh @@ -409,6 +409,31 @@ build_glew() GLEW_DEST=$DEPLOYDIR $MAKER install } +build_glib2() +{ + version="$1" + maj_min_version="${version%.*}" #Drop micro + + if [ -e $DEPLOYDIR/lib/glib-2.0 ]; then +echo "glib2 already installed. not building" + return +fi + +echo "Building glib2 $version..." + cd "$BASEDIR"/src + rm -rf "glib-$version" + if [ ! -f "glib-$version.tar.xz" ]; then +curl --insecure -LO "http://ftp.gnome.org/pub/gnome/sources/glib/$maj_min_version/glib-$version.tar.xz" + fi +tar xJf "glib-$version.tar.xz" + cd "glib-$version" + + ./configure --prefix="$DEPLOYDIR" + make -j$NUMCPU + make install + +} + build_opencsg() { if [ -e $DEPLOYDIR/lib/libopencsg.so ]; then From d46ce3fb8150fe01c0c07fac11ea2e9a8ed97038 Mon Sep 17 00:00:00 2001 From: Brody Kenrick Date: Fri, 6 Dec 2013 18:33:42 +1100 Subject: [PATCH 45/66] Add specific tests for unicode len() --- testdata/scad/misc/string-unicode.scad | 10 +++++++++- tests/regression/echotest/string-unicode-expected.echo | 3 +++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/testdata/scad/misc/string-unicode.scad b/testdata/scad/misc/string-unicode.scad index d8e3e5c9..1386d63d 100644 --- a/testdata/scad/misc/string-unicode.scad +++ b/testdata/scad/misc/string-unicode.scad @@ -1,3 +1,12 @@ +//Test length reporting +text_1bytes_len = "1234"; +text_2bytes_len = "ЛЛЛЛ"; +text_4bytes_len = "🂡🂱🃁🃑"; + +echo( "text_1bytes_len = ", text_1bytes_len, " len = ", len(text_1bytes_len) ); +echo( "text_2bytes_len = ", text_2bytes_len, " len = ", len(text_2bytes_len) ); +echo( "text_4bytes_len = ", text_4bytes_len, " len = ", len(text_4bytes_len) ); + //Test how well arrays of unicode string are accessed. texts_array = [ @@ -33,4 +42,3 @@ echo( "Past end of unicode only 4-byte ", text_4bytes[len(text_4bytes)] ); echo( "Past end of both 2-byte ", text_2bytes[ len(text_2bytes) * 2 ] ); echo( "Past end of both 4-byte ", text_4bytes[ len(text_4bytes) * 4 ] ); - diff --git a/tests/regression/echotest/string-unicode-expected.echo b/tests/regression/echotest/string-unicode-expected.echo index b4b848fd..a1cd3bec 100644 --- a/tests/regression/echotest/string-unicode-expected.echo +++ b/tests/regression/echotest/string-unicode-expected.echo @@ -1,3 +1,6 @@ +ECHO: "text_1bytes_len = ", "1234", " len = ", 4 +ECHO: "text_2bytes_len = ", "ЛЛЛЛ", " len = ", 4 +ECHO: "text_4bytes_len = ", "🂡🂱🃁🃑", " len = ", 4 ECHO: "[", 0, "] = ", "DEADBEEF", " of len=", 8, ":" ECHO: " [", 0, "]=", "D" ECHO: " [", 1, "]=", "E" From b7c818bf00b5c9e23ee97ce6669eb66a9b404562 Mon Sep 17 00:00:00 2001 From: a-e-m Date: Sun, 8 Dec 2013 16:15:50 -0800 Subject: [PATCH 46/66] Revert to uploading to dinkypage, gists won't work --- tests/test_pretty_print.py | 73 ++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 43 deletions(-) diff --git a/tests/test_pretty_print.py b/tests/test_pretty_print.py index b601c842..675867e7 100755 --- a/tests/test_pretty_print.py +++ b/tests/test_pretty_print.py @@ -36,12 +36,11 @@ import subprocess import time import platform try: - from urllib.request import urlopen, Request + from urllib.request import urlopen + from urllib.parse import urlencode except: - from urllib2 import urlopen, Request -import json -import base64 - + from urllib2 import urlopen + from urllib import urlencode def tryread(filename): data = None @@ -363,43 +362,32 @@ def to_html(project_name, startdate, tests, enddate, sysinfo, sysid, makefiles): # --- Web Upload --- -API_URL = 'https://api.github.com/%s' -# Username is personal access token, from https://github.com/settings/applications -# This way, no password is needed -USERNAME = '' # add OpenScad user token -PASSWORD = '' +def postify(data): + return urlencode(data).encode() -def make_auth(username, password): - auth = '%s:%s' % (USERNAME, PASSWORD) - return base64.b64encode(auth.encode()) - -def post_gist(name, content): - gist = '''{ - "description": "", - "public": true, - "files": { - "%s": { - "content": "%s" - } - } - }''' - gist = gist % (name, content) - - req = Request(API_URL % 'gists') - req.add_header('Authorization', b'Basic ' + make_auth(USERNAME, PASSWORD)) +def create_page(): + data = { + 'action': 'create', + 'type': 'html' + } try: - result = urlopen(req, data=gist) + response = urlopen('http://www.dinkypage.com', data=postify(data)) except: - print 'Could not upload results' return None - return json.loads(result.read()) - - -def get_raw_urls(result): - files = result.get('files', {}) - for file in files: - yield files[file].get('raw_url').replace('gist.github.com', 'rawgithub.com') + return response.geturl() +def upload_html(page_url, title, html): + data = { + 'mode': 'editor', + 'title': title, + 'html': html, + 'ajax': '1' + } + try: + response = urlopen(page_url, data=postify(data)) + except: + return False + return 'success' in response.read().decode() # --- End Web Upload --- @@ -458,13 +446,12 @@ def main(): debug('saving ' + html_filename + ' ' + str(len(html)) + ' bytes') trysave(html_filename, html) - result = post_gist(name=html_basename, content=html) - if result is None: + page_url = create_page() + if upload_html(page_url, title='OpenSCAD test results', html=html): + share_url = page_url.partition('?')[0] + print 'html report uploaded at', share_url + else: print 'could not upload html report' - return - - for url in get_raw_urls(result): - print 'html report uploaded at', url debug('test_pretty_print complete') From 58bf7386a11d6f6fb247cf95b89bbbb2682832ca Mon Sep 17 00:00:00 2001 From: a-e-m Date: Sun, 8 Dec 2013 16:32:36 -0800 Subject: [PATCH 47/66] Added --upload command line option --- tests/test_pretty_print.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/test_pretty_print.py b/tests/test_pretty_print.py index 675867e7..c0d35bb8 100755 --- a/tests/test_pretty_print.py +++ b/tests/test_pretty_print.py @@ -426,6 +426,11 @@ def main(): if not builddir: builddir = os.getcwd() debug('build dir set to ' + builddir) + + upload = False + if '--upload' in sys.argv: + upload = True + debug('will upload test report') # --- End Command Line Parsing --- @@ -446,12 +451,13 @@ def main(): debug('saving ' + html_filename + ' ' + str(len(html)) + ' bytes') trysave(html_filename, html) - page_url = create_page() - if upload_html(page_url, title='OpenSCAD test results', html=html): - share_url = page_url.partition('?')[0] - print 'html report uploaded at', share_url - else: - print 'could not upload html report' + if upload: + page_url = create_page() + if upload_html(page_url, title='OpenSCAD test results', html=html): + share_url = page_url.partition('?')[0] + print 'html report uploaded at', share_url + else: + print 'could not upload html report' debug('test_pretty_print complete') From a407e4bf29024dbed6f3e89376b8e7d134b98776 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Sun, 8 Dec 2013 23:13:28 -0500 Subject: [PATCH 48/66] Enable upload of test results (#525) --- tests/CMakeLists.txt | 6 ++++++ tests/CTestCustom.template | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f92eddf5..779ef08d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -904,6 +904,12 @@ string(REPLACE __header__ "Generated by cmake from ${CMAKE_CURRENT_SOURCE_DIR}/C string(REPLACE __cmake_system_name__ ${CMAKE_SYSTEM_NAME} TMP ${TMP}) string(REPLACE __openscad_binpath__ ${OPENSCAD_BINPATH} TMP ${TMP}) +set(OPENSCAD_UPLOAD_TESTS $ENV{OPENSCAD_UPLOAD_TESTS}) +if (OPENSCAD_UPLOAD_TESTS) + set(UPLOADARG "--upload") +endif() +string(REPLACE __openscad_upload_tests__ ${UPLOADARG} TMP ${TMP}) + if (MINGW_CROSS_ENV_DIR) string(REPLACE __wine__ wine TMP ${TMP}) else() diff --git a/tests/CTestCustom.template b/tests/CTestCustom.template index 3f82d734..a01f2b52 100644 --- a/tests/CTestCustom.template +++ b/tests/CTestCustom.template @@ -63,7 +63,12 @@ endif() message("running '__openscad_binpath__ --info' to generate sysinfo.txt") execute_process(COMMAND __wine__ __openscad_binpath__ --info OUTPUT_FILE sysinfo.txt) -set(CTEST_CUSTOM_POST_TEST ${CTEST_CUSTOM_POST_TEST} "__cmake_current_binary_dir__/test_pretty_print") + +if("${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}.${CMAKE_PATCH_VERSION}" VERSION_LESS 2.8.12) + set(CTEST_CUSTOM_POST_TEST ${CTEST_CUSTOM_POST_TEST} "__cmake_current_binary_dir__/test_pretty_print") +else() + set(CTEST_CUSTOM_POST_TEST ${CTEST_CUSTOM_POST_TEST} "__python__ __cmake_current_source_dir__/test_pretty_print.py --builddir=__cmake_current_binary_dir__ __openscad_upload_tests__") +endif() if ( ${debug_openscad_template} ) foreach(post_test ${CTEST_CUSTOM_POST_TEST} ) From 8f103043a280290802559ce8770c5984f7f0730a Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Sun, 8 Dec 2013 23:16:49 -0500 Subject: [PATCH 49/66] Upload test results from travis --- scripts/travis-ci.sh | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/scripts/travis-ci.sh b/scripts/travis-ci.sh index 9d4258ae..362c2dfa 100755 --- a/scripts/travis-ci.sh +++ b/scripts/travis-ci.sh @@ -1,12 +1,7 @@ #!/bin/bash -qmake && make -j4 -if [[ $? != 0 ]]; then - echo "Error building OpenSCAD executable" - exit 1 -fi cd tests -cmake . +cmake -DOPENSCAD_UPLOAD_TESTS=yes . if [[ $? != 0 ]]; then echo "Error configuring test suite" exit 1 From a22394fc392451919f5b378ffe85dbeac7fd3a22 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Mon, 9 Dec 2013 00:13:14 -0500 Subject: [PATCH 50/66] Set upload env. variable in travis env since the cmake version on Travis is too old --- .travis.yml | 2 ++ scripts/travis-ci.sh | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9442ca4f..5b9f2156 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,8 @@ before_install: - sudo apt-get install -qq build-essential libqt4-dev libqt4-opengl-dev libxmu-dev cmake bison flex git-core libboost-all-dev libXi-dev libmpfr-dev libboost-dev libglew-dev libeigen2-dev libeigen3-dev libcgal-dev libgmp3-dev libgmp-dev python-paramiko curl imagemagick - sudo apt-get install -qq libopencsg-dev +env: OPENSCAD_UPLOAD_TESTS=yes + branches: only: - travis diff --git a/scripts/travis-ci.sh b/scripts/travis-ci.sh index 362c2dfa..9f44b0c9 100755 --- a/scripts/travis-ci.sh +++ b/scripts/travis-ci.sh @@ -1,7 +1,7 @@ #!/bin/bash cd tests -cmake -DOPENSCAD_UPLOAD_TESTS=yes . +cmake . if [[ $? != 0 ]]; then echo "Error configuring test suite" exit 1 From 6fd378e9af2dab353764afc4bfad834bd14349b7 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Mon, 9 Dec 2013 00:24:55 -0500 Subject: [PATCH 51/66] Set upload env. variable in travis env since the cmake version on Travis is too old --- tests/test_pretty_print.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_pretty_print.py b/tests/test_pretty_print.py index c0d35bb8..0f86cb65 100755 --- a/tests/test_pretty_print.py +++ b/tests/test_pretty_print.py @@ -431,6 +431,11 @@ def main(): if '--upload' in sys.argv: upload = True debug('will upload test report') + + # Workaround for old cmake's not being able to pass parameters + # to CTEST_CUSTOM_POST_TEST + if bool(os.getenv("OPENSCAD_UPLOAD_TESTS")): + upload = True # --- End Command Line Parsing --- From e96305ddf5a728eb323a86e4e900752ae2a6ccfe Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Mon, 9 Dec 2013 00:46:38 -0500 Subject: [PATCH 52/66] minor tuning --- .travis.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5b9f2156..c499a8d7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: cpp -compiler: - - gcc +cache: apt +compiler: gcc + before_install: - echo 'yes' | sudo add-apt-repository ppa:chrysn/openscad - sudo apt-get update -qq @@ -11,6 +12,6 @@ env: OPENSCAD_UPLOAD_TESTS=yes branches: only: - - travis + - master script: ./scripts/travis-ci.sh From 7cb3ea77ff090e136eb4a9536a394b3766908d17 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Tue, 10 Dec 2013 23:08:04 -0500 Subject: [PATCH 53/66] Build glib2 and gettext on Mac, Find macro for glib2 --- scripts/macosx-build-dependencies.sh | 36 +++++++++++++++++++--------- tests/CMakeLists.txt | 23 +++++++----------- tests/FindGLIB2.cmake | 27 +++++++++++++++++++++ 3 files changed, 60 insertions(+), 26 deletions(-) create mode 100644 tests/FindGLIB2.cmake diff --git a/scripts/macosx-build-dependencies.sh b/scripts/macosx-build-dependencies.sh index 5e26feee..b61c6561 100755 --- a/scripts/macosx-build-dependencies.sh +++ b/scripts/macosx-build-dependencies.sh @@ -285,27 +285,40 @@ build_glew() make GLEW_DEST=$DEPLOYDIR CC=$CC CFLAGS.EXTRA="-no-cpp-precomp -dynamic -fno-common -mmacosx-version-min=$MAC_OSX_VERSION_MIN $GLEW_EXTRA_FLAGS -arch x86_64" LDFLAGS.EXTRA="-mmacosx-version-min=$MAC_OSX_VERSION_MIN $GLEW_EXTRA_FLAGS -arch x86_64" STRIP= install } +build_gettext() +{ + version=$1 + echo "Building gettext $version..." + + cd "$BASEDIR"/src + rm -rf "gettext-$version" + if [ ! -f "glib-$version.tar.xz" ]; then + curl --insecure -LO "http://ftpmirror.gnu.org/gettext/gettext-$version.tar.gz" + fi + tar xzf "gettext-$version.tar.gz" + cd "gettext-$version" + + ./configure --prefix="$DEPLOYDIR" + make -j4 + make install +} + build_glib2() { - version="$1" - maj_min_version="${version%.*}" #Drop micro + version=$1 + echo "Building glib2 $version..." - if [ -e $DEPLOYDIR/lib/glib-2.0 ]; then - echo "glib2 already installed. not building" - return - fi - - echo "Building glib2 $version..." cd "$BASEDIR"/src rm -rf "glib-$version" + maj_min_version="${version%.*}" #Drop micro if [ ! -f "glib-$version.tar.xz" ]; then curl --insecure -LO "http://ftp.gnome.org/pub/gnome/sources/glib/$maj_min_version/glib-$version.tar.xz" fi tar xJf "glib-$version.tar.xz" cd "glib-$version" - ./configure --prefix="$DEPLOYDIR" - make -j$NUMCPU + ./configure --disable-gtk-doc --disable-man --prefix="$DEPLOYDIR" CFLAGS="-I$DEPLOYDIR/include" LDFLAGS="-L$DEPLOYDIR/lib" + make -j4 make install } @@ -473,7 +486,8 @@ build_boost 1.54.0 # NB! For CGAL, also update the actual download URL in the function build_cgal 4.3 build_glew 1.10.0 -build_glib2 2.38.1 +build_gettext 0.18.3.1 +build_glib2 2.38.2 build_opencsg 1.3.2 if $OPTION_DEPLOY; then # build_sparkle andymatuschak 0ed83cf9f2eeb425d4fdd141c01a29d843970c20 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2533103e..e5900709 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -366,10 +366,10 @@ if (NOT $ENV{CGALDIR} STREQUAL "") elseif (NOT $ENV{OPENSCAD_LIBRARIES} STREQUAL "") if (EXISTS "$ENV{OPENSCAD_LIBRARIES}/lib/CGAL") set(CGAL_DIR "$ENV{OPENSCAD_LIBRARIES}/lib/CGAL") - set(CMAKE_MODULE_PATH "${CGAL_DIR}") + set(CMAKE_MODULE_PATH "${CGAL_DIR}" ${CMAKE_MODULE_PATH}) elseif (EXISTS "$ENV{OPENSCAD_LIBRARIES}/include/CGAL") set(CGAL_DIR "$ENV{OPENSCAD_LIBRARIES}") - set(CMAKE_MODULE_PATH "${CGAL_DIR}") + set(CMAKE_MODULE_PATH "${CGAL_DIR}" ${CMAKE_MODULE_PATH}) endif() endif() message(STATUS "CGAL_DIR: " ${CGAL_DIR}) @@ -413,16 +413,9 @@ if (NOT $ENV{OPENSCAD_LIBRARIES} STREQUAL "") set(ENV{PKG_CONFIG_LIBDIR} "$ENV{OPENSCAD_LIBRARIES}/lib/pkgconfig") endif() -# Find libraries (system installed or dependency built) using pkg-config -find_package(PkgConfig REQUIRED) - -#GLib-2 -pkg_search_module(GLIB2 REQUIRED glib-2.0>=2.2.0) -#Can't use the CXXFlags directly as they are ;-separated -string(REPLACE ";" " " GLIB2_CFLAGS "${GLIB2_CFLAGS}") -message(STATUS "glib-2.0 found: ${GLIB2_VERSION}") - -add_definitions(${GLIB2_CFLAGS}) +find_package(GLIB2 2.2.0 REQUIRED) +add_definitions(${GLIB2_DEFINITIONS}) +inclusion(GLIB2_DIR GLIB2_INCLUDE_DIRS) # Imagemagick @@ -595,7 +588,7 @@ set(OFFSCREEN_SOURCES ../src/OpenCSGRenderer.cc) add_library(tests-core STATIC ${CORE_SOURCES}) -target_link_libraries(tests-core ${OPENGL_LIBRARIES} ${GLIB2_LIBRARIES} ) +target_link_libraries(tests-core) set(TESTS-CORE-LIBRARIES ${OPENGL_LIBRARIES} ${GLIB2_LIBRARIES} ${Boost_LIBRARIES} ) add_library(tests-common STATIC ${COMMON_SOURCES}) @@ -616,7 +609,7 @@ set(TESTS-NOCGAL-LIBRARIES ${TESTS-CORE-LIBRARIES}) # modulecachetest # add_executable(modulecachetest modulecachetest.cc) -target_link_libraries(modulecachetest tests-nocgal ${TESTS-NOCGAL-LIBRARIES} ${Boost_LIBRARIES}) +target_link_libraries(modulecachetest tests-nocgal ${TESTS-NOCGAL-LIBRARIES}) # # csgtexttest @@ -636,7 +629,7 @@ target_link_libraries(cgalcachetest tests-cgal ${TESTS-CGAL-LIBRARIES} ${GLEW_LI # add_executable(openscad_nogui ../src/openscad.cc) set_target_properties(openscad_nogui PROPERTIES COMPILE_FLAGS "-fno-strict-aliasing -DEIGEN_DONT_ALIGN -DENABLE_CGAL -DENABLE_OPENCSG ${CGAL_CXX_FLAGS_INIT}") -target_link_libraries(openscad_nogui tests-offscreen tests-cgal tests-nocgal ${TESTS-CORE-LIBRARIES} ${TESTS-CGAL-LIBRARIES} ${GLEW_LIBRARY} ${Boost_LIBRARIES} ${OPENCSG_LIBRARY} ${COCOA_LIBRARY} ) +target_link_libraries(openscad_nogui tests-offscreen tests-cgal tests-nocgal ${TESTS-CORE-LIBRARIES} ${TESTS-CGAL-LIBRARIES} ${GLEW_LIBRARY} ${OPENCSG_LIBRARY} ${COCOA_LIBRARY} ) # # GUI binary tests diff --git a/tests/FindGLIB2.cmake b/tests/FindGLIB2.cmake new file mode 100644 index 00000000..b27b9a9b --- /dev/null +++ b/tests/FindGLIB2.cmake @@ -0,0 +1,27 @@ +find_package(PkgConfig REQUIRED) + +pkg_search_module(GLIB2 REQUIRED glib-2.0) +#message("GLIB2_LIBRARIES ${GLIB2_LIBRARIES}") +#message("GLIB2_LIBRARY_DIRS ${GLIB2_LIBRARY_DIRS}") +#message("GLIB2_LDFLAGS ${GLIB2_LDFLAGS}") +#message("GLIB2_LDFLAGS_OTHER ${GLIB2_LDFLAGS_OTHER}") +message("GLIB2_INCLUDE_DIRS ${GLIB2_INCLUDE_DIRS}") +#message("GLIB2_CFLAGS ${GLIB2_CFLAGS}") +#message("GLIB2_CFLAGS_OTHER ${GLIB2_CFLAGS_OTHER}") +#message("GLIB2_LIBDIR ${GLIB2_LIBDIR}") + +set(GLIB2_DEFINITIONS ${GLIB2_CFLAGS_OTHER}) +#message("GLIB2_DEFINITIONS ${GLIB2_DEFINITIONS}") + +set(GLIB2_LIBRARY_NAMES ${GLIB2_LIBRARIES}) +set(GLIB2_LIBRARIES "") +foreach(GLIB2_LIB ${GLIB2_LIBRARY_NAMES}) +# message("lib: ${GLIB2_LIB}") + set(TMP TMP-NOTFOUND) + find_library(TMP NAMES ${GLIB2_LIB} + PATHS ${GLIB2_LIBRARY_DIRS} + NO_DEFAULT_PATH) +# message("TMP: ${TMP}") + list(APPEND GLIB2_LIBRARIES "${TMP}") +endforeach() +message("GLIB2_LIBRARIES: ${GLIB2_LIBRARIES}") From 379e7a05472a50cb417fd0582d2ca4466b2365fe Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Wed, 11 Dec 2013 00:16:27 -0500 Subject: [PATCH 54/66] Updated gmp patch. Fixes #558 --- scripts/macosx-build-dependencies.sh | 81 +++++++++++++++------------- 1 file changed, 45 insertions(+), 36 deletions(-) diff --git a/scripts/macosx-build-dependencies.sh b/scripts/macosx-build-dependencies.sh index d4ca1f7c..eadea9f0 100755 --- a/scripts/macosx-build-dependencies.sh +++ b/scripts/macosx-build-dependencies.sh @@ -83,6 +83,51 @@ build_gmp() fi tar xjf gmp-$version.tar.bz2 cd gmp-$version + patch -p0 gmp-h.in << EOF +--- gmp-5.1.3/gmp-h.in.old 2013-12-02 20:16:26.000000000 -0800 ++++ gmp-5.1.3/gmp-h.in 2013-12-02 20:21:22.000000000 -0800 +@@ -27,13 +27,38 @@ + #endif + + +-/* Instantiated by configure. */ + #if ! defined (__GMP_WITHIN_CONFIGURE) ++/* For benefit of fat builds on MacOSX, generate a .h file that can ++ * be used with a universal fat library ++ */ ++#if defined(__x86_64__) ++#define __GMP_HAVE_HOST_CPU_FAMILY_power 0 ++#define __GMP_HAVE_HOST_CPU_FAMILY_powerpc 0 ++#define GMP_LIMB_BITS 64 ++#define GMP_NAIL_BITS 0 ++#elif defined(__i386__) ++#define __GMP_HAVE_HOST_CPU_FAMILY_power 0 ++#define __GMP_HAVE_HOST_CPU_FAMILY_powerpc 0 ++#define GMP_LIMB_BITS 32 ++#define GMP_NAIL_BITS 0 ++#elif defined(__powerpc64__) ++#define __GMP_HAVE_HOST_CPU_FAMILY_power 0 ++#define __GMP_HAVE_HOST_CPU_FAMILY_powerpc 1 ++#define GMP_LIMB_BITS 64 ++#define GMP_NAIL_BITS 0 ++#elif defined(__ppc__) ++#define __GMP_HAVE_HOST_CPU_FAMILY_power 0 ++#define __GMP_HAVE_HOST_CPU_FAMILY_powerpc 1 ++#define GMP_LIMB_BITS 32 ++#define GMP_NAIL_BITS 0 ++#else ++/* For other architectures, fall back on values computed by configure */ + #define __GMP_HAVE_HOST_CPU_FAMILY_power @HAVE_HOST_CPU_FAMILY_power@ + #define __GMP_HAVE_HOST_CPU_FAMILY_powerpc @HAVE_HOST_CPU_FAMILY_powerpc@ + #define GMP_LIMB_BITS @GMP_LIMB_BITS@ + #define GMP_NAIL_BITS @GMP_NAIL_BITS@ + #endif ++#endif + #define GMP_NUMB_BITS (GMP_LIMB_BITS - GMP_NAIL_BITS) + #define GMP_NUMB_MASK ((~ __GMP_CAST (mp_limb_t, 0)) >> GMP_NAIL_BITS) + #define GMP_NUMB_MAX GMP_NUMB_MASK +EOF + if $OPTION_32BIT; then mkdir build-i386 cd build-i386 @@ -119,42 +164,6 @@ build_gmp() mkdir -p include cp x86_64/include/gmp.h include/ cp x86_64/include/gmpxx.h include/ - - patch -p0 include/gmp.h << EOF ---- gmp.h.orig 2011-11-08 01:03:41.000000000 +0100 -+++ gmp.h 2011-11-08 01:06:21.000000000 +0100 -@@ -26,12 +26,28 @@ - #endif - - --/* Instantiated by configure. */ --#if ! defined (__GMP_WITHIN_CONFIGURE) -+#if defined(__i386__) -+#define __GMP_HAVE_HOST_CPU_FAMILY_power 0 -+#define __GMP_HAVE_HOST_CPU_FAMILY_powerpc 0 -+#define GMP_LIMB_BITS 32 -+#define GMP_NAIL_BITS 0 -+#elif defined(__x86_64__) - #define __GMP_HAVE_HOST_CPU_FAMILY_power 0 - #define __GMP_HAVE_HOST_CPU_FAMILY_powerpc 0 - #define GMP_LIMB_BITS 64 - #define GMP_NAIL_BITS 0 -+#elif defined(__ppc__) -+#define __GMP_HAVE_HOST_CPU_FAMILY_power 0 -+#define __GMP_HAVE_HOST_CPU_FAMILY_powerpc 1 -+#define GMP_LIMB_BITS 32 -+#define GMP_NAIL_BITS 0 -+#elif defined(__powerpc64__) -+#define __GMP_HAVE_HOST_CPU_FAMILY_power 0 -+#define __GMP_HAVE_HOST_CPU_FAMILY_powerpc 1 -+#define GMP_LIMB_BITS 64 -+#define GMP_NAIL_BITS 0 -+#else -+#error Unsupported architecture - #endif - #define GMP_NUMB_BITS (GMP_LIMB_BITS - GMP_NAIL_BITS) - #define GMP_NUMB_MASK ((~ __GMP_CAST (mp_limb_t, 0)) >> GMP_NAIL_BITS) -EOF } # As with gmplib, mpfr is built separately in 32-bit and 64-bit mode and then merged From 745664f1266b3a46898c671ba17db21df19adebf Mon Sep 17 00:00:00 2001 From: Brody Kenrick Date: Wed, 11 Dec 2013 17:06:27 +1100 Subject: [PATCH 55/66] Ctests build for Glib2 - Add GLIB2_LIBDIR to find_library paths --- tests/FindGLIB2.cmake | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/FindGLIB2.cmake b/tests/FindGLIB2.cmake index b27b9a9b..9164c391 100644 --- a/tests/FindGLIB2.cmake +++ b/tests/FindGLIB2.cmake @@ -2,13 +2,13 @@ find_package(PkgConfig REQUIRED) pkg_search_module(GLIB2 REQUIRED glib-2.0) #message("GLIB2_LIBRARIES ${GLIB2_LIBRARIES}") -#message("GLIB2_LIBRARY_DIRS ${GLIB2_LIBRARY_DIRS}") +message("GLIB2_LIBRARY_DIRS ${GLIB2_LIBRARY_DIRS}") #message("GLIB2_LDFLAGS ${GLIB2_LDFLAGS}") #message("GLIB2_LDFLAGS_OTHER ${GLIB2_LDFLAGS_OTHER}") message("GLIB2_INCLUDE_DIRS ${GLIB2_INCLUDE_DIRS}") #message("GLIB2_CFLAGS ${GLIB2_CFLAGS}") #message("GLIB2_CFLAGS_OTHER ${GLIB2_CFLAGS_OTHER}") -#message("GLIB2_LIBDIR ${GLIB2_LIBDIR}") +message("GLIB2_LIBDIR ${GLIB2_LIBDIR}") set(GLIB2_DEFINITIONS ${GLIB2_CFLAGS_OTHER}) #message("GLIB2_DEFINITIONS ${GLIB2_DEFINITIONS}") @@ -20,6 +20,7 @@ foreach(GLIB2_LIB ${GLIB2_LIBRARY_NAMES}) set(TMP TMP-NOTFOUND) find_library(TMP NAMES ${GLIB2_LIB} PATHS ${GLIB2_LIBRARY_DIRS} + PATHS ${GLIB2_LIBDIR} NO_DEFAULT_PATH) # message("TMP: ${TMP}") list(APPEND GLIB2_LIBRARIES "${TMP}") From 225afc966be6a99453b2ff2f77c621b7db01b12e Mon Sep 17 00:00:00 2001 From: Brody Kenrick Date: Wed, 11 Dec 2013 17:53:43 +1100 Subject: [PATCH 56/66] Add gettext lib for linux also --- scripts/uni-build-dependencies.sh | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/scripts/uni-build-dependencies.sh b/scripts/uni-build-dependencies.sh index ba328b7e..5b9e1290 100755 --- a/scripts/uni-build-dependencies.sh +++ b/scripts/uni-build-dependencies.sh @@ -409,6 +409,24 @@ build_glew() GLEW_DEST=$DEPLOYDIR $MAKER install } +build_gettext() +{ + version=$1 + echo "Building gettext $version..." + + cd "$BASEDIR"/src + rm -rf "gettext-$version" + if [ ! -f "glib-$version.tar.xz" ]; then + curl --insecure -LO "http://ftpmirror.gnu.org/gettext/gettext-$version.tar.gz" + fi + tar xzf "gettext-$version.tar.gz" + cd "gettext-$version" + + ./configure --prefix="$DEPLOYDIR" + make -j4 + make install +} + build_glib2() { version="$1" @@ -428,10 +446,9 @@ curl --insecure -LO "http://ftp.gnome.org/pub/gnome/sources/glib/$maj_min_versio tar xJf "glib-$version.tar.xz" cd "glib-$version" - ./configure --prefix="$DEPLOYDIR" + ./configure --disable-gtk-doc --disable-man --prefix="$DEPLOYDIR" CFLAGS="-I$DEPLOYDIR/include" LDFLAGS="-L$DEPLOYDIR/lib" make -j$NUMCPU make install - } build_opencsg() @@ -628,6 +645,7 @@ build_boost 1.53.0 build_cgal 4.0.2 build_glew 1.9.0 build_opencsg 1.3.2 +build_gettext 0.18.3.1 build_glib2 2.38.2 echo "OpenSCAD dependencies built and installed to " $BASEDIR From 4552d2d26352330ea0786dc65bc8f49328f90e02 Mon Sep 17 00:00:00 2001 From: Brody Kenrick Date: Wed, 11 Dec 2013 17:59:38 +1100 Subject: [PATCH 57/66] Add back in GL and GLIB2 links for tests-core --- tests/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e5900709..519f1e17 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -588,7 +588,7 @@ set(OFFSCREEN_SOURCES ../src/OpenCSGRenderer.cc) add_library(tests-core STATIC ${CORE_SOURCES}) -target_link_libraries(tests-core) +target_link_libraries(tests-core ${OPENGL_LIBRARIES} ${GLIB2_LIBRARIES} ) set(TESTS-CORE-LIBRARIES ${OPENGL_LIBRARIES} ${GLIB2_LIBRARIES} ${Boost_LIBRARIES} ) add_library(tests-common STATIC ${COMMON_SOURCES}) From d14f0be01c06a872a7fb0cef6e0fa67ad7bc4a4f Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Wed, 11 Dec 2013 02:01:50 -0500 Subject: [PATCH 58/66] minor fix: Only use UPLOADARG if it exists --- tests/CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 779ef08d..370c8be2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -908,7 +908,9 @@ set(OPENSCAD_UPLOAD_TESTS $ENV{OPENSCAD_UPLOAD_TESTS}) if (OPENSCAD_UPLOAD_TESTS) set(UPLOADARG "--upload") endif() -string(REPLACE __openscad_upload_tests__ ${UPLOADARG} TMP ${TMP}) +if (UPLOADARG) + string(REPLACE __openscad_upload_tests__ ${UPLOADARG} TMP ${TMP}) +endif() if (MINGW_CROSS_ENV_DIR) string(REPLACE __wine__ wine TMP ${TMP}) From 966f7eb6242cca891b52a0fe4f52775a086c9cd9 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Thu, 12 Dec 2013 01:15:34 -0500 Subject: [PATCH 59/66] #514 Change gcc 4.8.2 version check error to a warning --- src/version_check.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/version_check.h b/src/version_check.h index be52e61d..2688f2a5 100644 --- a/src/version_check.h +++ b/src/version_check.h @@ -113,7 +113,7 @@ a time, to avoid confusion. + __GNUC_MINOR__ * 100 \ + __GNUC_PATCHLEVEL__) #if GCC_VERSION == 40802 -#error "OpenSCAD isnt compatible with gcc 4.8.2. Please try a different version" +#warning "gcc 4.8.2 contains a bug causing a crash in CGAL." #endif #endif // OPENSCAD_SKIP_VERSION_CHECK From 68c706da8c9ed5d7bc32d41d7cce86531eb2a673 Mon Sep 17 00:00:00 2001 From: Brody Kenrick Date: Thu, 12 Dec 2013 17:48:55 +1100 Subject: [PATCH 60/66] Fix check_dependencies for Ubuntu 12.10 and ... ... and other platforms that report a different arch triplet than where they store their libraries) check-dependencies.sh doesn't find glib2 on Ubuntu 12.10 since glib is installed in /usr/lib/i386-linux-gnu/glib-2.0/include/glibconfig.h while gcc -dumpmachine reports the arch as i686-linux-gnu --- scripts/check-dependencies.sh | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/scripts/check-dependencies.sh b/scripts/check-dependencies.sh index e5871989..c4312553 100755 --- a/scripts/check-dependencies.sh +++ b/scripts/check-dependencies.sh @@ -74,7 +74,19 @@ glib2_sysver() glib2archtriplet=`dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null` fi glib2path=$1/lib/$glib2archtriplet/glib-2.0/include/glibconfig.h - if [ ! -e $glib2path ]; then return; fi + if [ ! -e $glib2path ]; then + #No glib found + #glib can be installed in /usr/lib/i386-linux-gnu/glib-2.0/ on arch i686-linux-gnu (sometimes?) + if [ $glib2archtriplet = "i686-linux-gnu" ]; then + glib2archtriplet=i386-linux-gnu + glib2path=$1/lib/$glib2archtriplet/glib-2.0/include/glibconfig.h + if [ ! -e $glib2path ]; then + return; + fi + else + return; + fi + fi glib2major=`grep "define *GLIB_MAJOR_VERSION *[0-9.]*" $glib2path | awk '{print $3}'` glib2minor=`grep "define *GLIB_MINOR_VERSION *[0-9.]*" $glib2path | awk '{print $3}'` glib2micro=`grep "define *GLIB_MICRO_VERSION *[0-9.]*" $glib2path | awk '{print $3}'` From b8859a6716f2e42395ca96ca5c6af10e0d02cec8 Mon Sep 17 00:00:00 2001 From: Brody Kenrick Date: Thu, 12 Dec 2013 17:54:40 +1100 Subject: [PATCH 61/66] Correct coding style for last commit (check_dependencies). --- scripts/check-dependencies.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/check-dependencies.sh b/scripts/check-dependencies.sh index c4312553..72c4c749 100755 --- a/scripts/check-dependencies.sh +++ b/scripts/check-dependencies.sh @@ -80,9 +80,7 @@ glib2_sysver() if [ $glib2archtriplet = "i686-linux-gnu" ]; then glib2archtriplet=i386-linux-gnu glib2path=$1/lib/$glib2archtriplet/glib-2.0/include/glibconfig.h - if [ ! -e $glib2path ]; then - return; - fi + if [ ! -e $glib2path ]; then return; fi else return; fi From 585412bed623dc6adc8df5f27131997e6d2541a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Fri, 13 Dec 2013 15:51:58 +0100 Subject: [PATCH 62/66] Change invalid Programming category to Development Programming is not a valid category of .desktop file, it is not listed in either [Main Categories](http://standards.freedesktop.org/menu-spec/latest/apa.html) or [Additional Categories](http://standards.freedesktop.org/menu-spec/latest/apas02.html). desktop-file-validate returns error: openscad.desktop: error: value "Graphics;3DGraphics;Engineering;Programming;" for key "Categories" in group "Desktop Entry" contains an unregistered value "Programming"; values extending the format should start with "X-" This changes it to Development, witch is a valid category listed in Additional Categories. Now desktop-file-validate only gives us a hint: openscad.desktop: hint: value "Graphics;3DGraphics;Engineering;Development;" for key "Categories" in group "Desktop Entry" contains more than one main category; application might appear more than once in the application menu. That is fine, as we want it both in Graphics and Development. This is a valid response to #533 --- icons/openscad.desktop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/icons/openscad.desktop b/icons/openscad.desktop index f0282acd..46150a21 100644 --- a/icons/openscad.desktop +++ b/icons/openscad.desktop @@ -4,4 +4,4 @@ Version=1.0 Name=OpenSCAD Icon=openscad Exec=openscad %f -Categories=Graphics;3DGraphics;Engineering;Programming; +Categories=Graphics;3DGraphics;Engineering;Development; From a3aa61bab2c88fce649a9515dec0ddb7d15be085 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Sat, 14 Dec 2013 12:49:23 -0500 Subject: [PATCH 63/66] bugfix: glib include fix --- glib-2.0.pri | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/glib-2.0.pri b/glib-2.0.pri index 0fbc4e2d..2f293bf6 100644 --- a/glib-2.0.pri +++ b/glib-2.0.pri @@ -7,14 +7,13 @@ # 3. system's standard include paths from pkg-config glib-2.0 { - # read environment variables OPENSCAD_LIBRARIES_DIR = $$(OPENSCAD_LIBRARIES) GLIB2_DIR = $$(GLIB2DIR) !isEmpty(OPENSCAD_LIBRARIES_DIR) { isEmpty(GLIB2_INCLUDEPATH) { - GLIB2_INCLUDEPATH_1 = $$OPENSCAD_LIBRARIES_DIR/include/glib-2.0 + GLIB2_INCLUDEPATH = $$OPENSCAD_LIBRARIES_DIR/include/glib-2.0 GLIB2_INCLUDEPATH_2 = $$OPENSCAD_LIBRARIES_DIR/lib/glib-2.0/include GLIB2_LIBPATH = $$OPENSCAD_LIBRARIES_DIR/lib } @@ -23,7 +22,7 @@ GLIB2_DIR = $$(GLIB2DIR) isEmpty(GLIB2_INCLUDEPATH) { GLIB2_CFLAGS = $$system("pkg-config --cflags glib-2.0") } else { - GLIB2_CFLAGS = -I$$GLIB2_INCLUDEPATH_1 + GLIB2_CFLAGS = -I$$GLIB2_INCLUDEPATH GLIB2_CFLAGS += -I$$GLIB2_INCLUDEPATH_2 } @@ -35,4 +34,6 @@ isEmpty(GLIB2_LIBPATH) { QMAKE_CXXFLAGS += $$GLIB2_CFLAGS LIBS += $$GLIB2_LIBS + +message("glib: $$GLIB2_CFLAGS") } From cca80a15590c24de9bfd4654b19a64bf1426cf54 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Sat, 14 Dec 2013 17:44:05 -0500 Subject: [PATCH 64/66] bugfix: #562 didn't take into account that it's allowed with all X coordinates being negative --- src/PolySetCGALEvaluator.cc | 12 +++++++----- .../scad/features/rotate_extrude-tests.scad | 3 +++ .../rotate_extrude-tests-expected.png | Bin 17684 -> 18494 bytes .../rotate_extrude-tests-expected.csg | 7 +++++++ .../rotate_extrude-tests-expected.png | Bin 18320 -> 19174 bytes .../rotate_extrude-tests-expected.png | Bin 8198 -> 8132 bytes 6 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/PolySetCGALEvaluator.cc b/src/PolySetCGALEvaluator.cc index 599fd7f5..a2d896d8 100644 --- a/src/PolySetCGALEvaluator.cc +++ b/src/PolySetCGALEvaluator.cc @@ -455,19 +455,21 @@ PolySet *PolySetCGALEvaluator::rotateDxfData(const RotateExtrudeNode &node, DxfD for (size_t i = 0; i < dxf.paths.size(); i++) { + double min_x = 0; double max_x = 0; for (size_t j = 0; j < dxf.paths[i].indices.size(); j++) { double point_x = dxf.points[dxf.paths[i].indices[j]][0]; - if (point_x < 0) { - PRINT("ERROR: all points for rotate_extrude() must have non-negative X coordinates"); - PRINTB("[Point %d on path %d has X coordinate %f]", j % i % point_x); + min_x = fmin(min_x, point_x); + max_x = fmax(max_x, point_x); + + if ((max_x - min_x) > max_x && (max_x - min_x) > fabs(min_x)) { + PRINTB("ERROR: all points for rotate_extrude() must have the same X coordinate sign (range is %.2f -> %.2f)", min_x % max_x); delete ps; return NULL; } - max_x = fmax(max_x, point_x); } - int fragments = get_fragments_from_r(max_x, node.fn, node.fs, node.fa); + int fragments = get_fragments_from_r(max_x-min_x, node.fn, node.fs, node.fa); double ***points; points = new double**[fragments]; diff --git a/testdata/scad/features/rotate_extrude-tests.scad b/testdata/scad/features/rotate_extrude-tests.scad index 010b7d22..ec8d1cce 100644 --- a/testdata/scad/features/rotate_extrude-tests.scad +++ b/testdata/scad/features/rotate_extrude-tests.scad @@ -32,3 +32,6 @@ translate([50,50,0]) { // Minimal $fn translate([0,-60,0]) rotate_extrude($fn=1) translate([20,0,0]) circle(r=10,$fn=1); +// Object in negative X +translate([0,60,0]) rotate_extrude() translate([-20,0]) square(10); + diff --git a/tests/regression/cgalpngtest/rotate_extrude-tests-expected.png b/tests/regression/cgalpngtest/rotate_extrude-tests-expected.png index 1488c85ee1a3191248fb329596d427ef2054a276..d198344f2e1fe1088a515b89ac803a010770c4e1 100644 GIT binary patch literal 18494 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 literal 17684 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 diff --git a/tests/regression/dumptest/rotate_extrude-tests-expected.csg b/tests/regression/dumptest/rotate_extrude-tests-expected.csg index a86dd8f0..d010bfd6 100644 --- a/tests/regression/dumptest/rotate_extrude-tests-expected.csg +++ b/tests/regression/dumptest/rotate_extrude-tests-expected.csg @@ -50,4 +50,11 @@ group() { } } } + multmatrix([[1, 0, 0, 0], [0, 1, 0, 60], [0, 0, 1, 0], [0, 0, 0, 1]]) { + rotate_extrude(convexity = 1, $fn = 0, $fa = 12, $fs = 2) { + multmatrix([[1, 0, 0, -20], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) { + square(size = [10, 10], center = false); + } + } + } } diff --git a/tests/regression/opencsgtest/rotate_extrude-tests-expected.png b/tests/regression/opencsgtest/rotate_extrude-tests-expected.png index 861f6ab42b264580b863b3cd597b95218c60806e..0be247eeb63592cbf5b0c34653b9af94cfdea734 100644 GIT binary patch literal 19174 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 literal 18320 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 TW8o_Nz7MRn*;|yEGmiZq;&J^d From 0eaa5333dbab54f81b6a53f24b06d7c8f46f10c9 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Sat, 14 Dec 2013 17:57:28 -0500 Subject: [PATCH 65/66] Put back printing path to test report --- tests/test_pretty_print.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_pretty_print.py b/tests/test_pretty_print.py index 0f86cb65..4c3993cc 100755 --- a/tests/test_pretty_print.py +++ b/tests/test_pretty_print.py @@ -455,6 +455,7 @@ def main(): html_filename = os.path.join(builddir, 'Testing', 'Temporary', html_basename) debug('saving ' + html_filename + ' ' + str(len(html)) + ' bytes') trysave(html_filename, html) + print "report saved:\n", html_filename.replace(os.getcwd()+os.path.sep,'') if upload: page_url = create_page() From c7cea0082e427f3c53985845f05e8737873c8a25 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Sun, 15 Dec 2013 15:02:32 -0500 Subject: [PATCH 66/66] Added testcase for concave polygons in polyhedrons --- testdata/scad/features/polyhedron-tests.scad | 27 +++++++++++++++++- .../cgalpngtest/polyhedron-tests-expected.png | Bin 9208 -> 8654 bytes .../dumptest/polyhedron-tests-expected.csg | 8 +++++- .../opencsgtest/polyhedron-tests-expected.png | Bin 10415 -> 9915 bytes .../polyhedron-tests-expected.png | Bin 6905 -> 10536 bytes 5 files changed, 33 insertions(+), 2 deletions(-) diff --git a/testdata/scad/features/polyhedron-tests.scad b/testdata/scad/features/polyhedron-tests.scad index 690d9627..1f11d7ef 100644 --- a/testdata/scad/features/polyhedron-tests.scad +++ b/testdata/scad/features/polyhedron-tests.scad @@ -11,10 +11,35 @@ module polyhedrons() { translate([4,0,0]) polyhedron(points = [[1,0,0],[-1,0,0],[0,1,0],[0,-1,0],[0,0,1],[0,0,-1]], triangles = [[0,2,4],[0,5,2],[0,4,3],[0,3,5],[1,4,2],[1,2,5],[1,3,4], [1,5,3]]); + +// Containing concave polygons +translate([6,0,0]) +polyhedron(points=[ + [-0.8,-0.8,-0.8], + [0,0,-0.8], + [0.8,-0.8,-0.8], + [0.8,0.8,-0.8], + [-0.8,0.8,-0.8], + [-0.8,-0.8,0.8], + [0,0,0.8], + [0.8,-0.8,0.8], + [0.8,0.8,0.8], + [-0.8,0.8,0.8], + ], + triangles=[ + [0,1,2,3,4], + [5,6,1,0], + [6,7,2,1], + [7,8,3,2], + [8,9,4,3], + [9,5,0,4], + [9,8,7,6,5], + ], convexity=2); } polyhedrons(); translate([0,2,0]) difference() { polyhedrons(); - translate([2,0,2]) cube([8,3,3], center=true); + translate([3,0,2]) cube([8,3,3], center=true); } + diff --git a/tests/regression/cgalpngtest/polyhedron-tests-expected.png b/tests/regression/cgalpngtest/polyhedron-tests-expected.png index c80990fa3dd217fde7dddf81cfbb94f189057333..322160db97a2142b33f049d29d29dfdd76bb33af 100644 GIT binary patch literal 8654 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 Dp5^rS literal 9208 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 R;07dg#?b6!*>Tq!{{wzMA@u+N literal 6905 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