├── .gitignore ├── .gitmodules ├── AUTHORS ├── CMakeLists.txt ├── COPYING ├── ChangeLog ├── INSTALL ├── LICENSE ├── NEWS ├── README ├── cmake.h.in ├── cmake └── CXXSniffer.cmake ├── doc ├── CMakeLists.txt ├── demo │ ├── 0.txt │ ├── 1.txt │ ├── 2.txt │ ├── 3.txt │ ├── 4.txt │ ├── 5.txt │ ├── 6.txt │ ├── hamlet │ └── intro.txt └── man │ ├── .gitignore │ └── clog.1.in ├── src ├── .gitignore ├── CMakeLists.txt ├── Rule.cpp ├── Rule.h ├── clog.cpp └── rules.cpp └── test ├── .gitignore ├── CMakeLists.txt ├── basetest ├── __init__.py ├── clog.py ├── compat.py ├── exceptions.py ├── meta.py ├── testing.py └── utils.py ├── blank.t ├── decorate.t ├── pattern.t ├── problems ├── regex.t ├── rule.t.cpp ├── run_all ├── simpletap └── __init__.py ├── suppress.t ├── template.t ├── test.cpp ├── test.h └── version.t /.gitignore: -------------------------------------------------------------------------------- 1 | CMakeFiles 2 | CMakeCache.txt 3 | CPack* 4 | cmake_install.cmake 5 | Makefile 6 | install_manifest.txt 7 | cmake.h 8 | commit.h 9 | _CPack_Packages 10 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/libshared"] 2 | path = src/libshared 3 | url = https://github.com/GothenburgBitFactory/libshared.git 4 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | The development of clog was made possible by the significant contributions of 2 | the following people: 3 | 4 | Paul Beckingham (Principal Author) 5 | Federico Hernandez (Principal Author) 6 | 7 | The following submitted code, packages or analysis, and deserve special thanks: 8 | 9 | John West 10 | Jakub Wilk 11 | Jörg Krause 12 | Ben Boeckel 13 | Paul J. Fenwick 14 | Michael Neumann 15 | 16 | Thanks to the following, who submitted detailed bug reports and excellent 17 | suggestions: 18 | 19 | vkrishn 20 | Rainer Müller 21 | Thomas Sattler 22 | Cory Donnelly 23 | P4 24 | David Patrick 25 | hosaka 26 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 2.8) 2 | set (CMAKE_LEGACY_CYGWIN_WIN32 0) # Remove when CMake >= 2.8.4 is required 3 | set (CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake") 4 | set (HAVE_CMAKE true) 5 | 6 | project (clog) 7 | include (CXXSniffer) 8 | 9 | set (PROJECT_VERSION "1.4.0") 10 | 11 | set (PACKAGE "${PROJECT_NAME}") 12 | set (VERSION "${PROJECT_VERSION}") 13 | set (PACKAGE_BUGREPORT "support@taskwarrior.org") 14 | set (PACKAGE_NAME "${PACKAGE}") 15 | set (PACKAGE_TARNAME "${PACKAGE}") 16 | set (PACKAGE_VERSION "${VERSION}") 17 | set (PACKAGE_STRING "${PACKAGE} ${VERSION}") 18 | 19 | SET (CLOG_DOCDIR share/doc/clog CACHE STRING "Installation directory for doc files") 20 | SET (CLOG_BINDIR bin CACHE STRING "Installation directory for the binary") 21 | 22 | message ("-- Configuring cmake.h") 23 | configure_file ( 24 | ${CMAKE_SOURCE_DIR}/cmake.h.in 25 | ${CMAKE_SOURCE_DIR}/cmake.h) 26 | 27 | add_subdirectory (src) 28 | add_subdirectory (doc) 29 | if (EXISTS ${CMAKE_SOURCE_DIR}/test) 30 | add_subdirectory (test EXCLUDE_FROM_ALL) 31 | endif (EXISTS ${CMAKE_SOURCE_DIR}/test) 32 | 33 | set (doc_FILES NEWS ChangeLog README INSTALL AUTHORS COPYING) 34 | foreach (doc_FILE ${doc_FILES}) 35 | install (FILES ${doc_FILE} DESTINATION ${CLOG_DOCDIR}) 36 | endforeach (doc_FILE) 37 | 38 | # --- 39 | 40 | set (CPACK_SOURCE_GENERATOR "TGZ") 41 | set (CPACK_SOURCE_PACKAGE_FILE_NAME ${PACKAGE_NAME}-${PACKAGE_VERSION}) 42 | set (CPACK_SOURCE_IGNORE_FILES "CMakeCache" "CMakeFiles" "CPackConfig" "CPackSourceConfig" 43 | "_CPack_Packages" "cmake_install" "install_manifest" 44 | "Makefile$" "test" 45 | "/\\\\.gitignore" "/\\\\.git/" "swp$") 46 | include (CPack) 47 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | 2 | Copyright 2010 - 2017, Göteborg Bit Factory. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | 22 | http://www.opensource.org/licenses/mit-license.php 23 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 2 | 1.4.0 () 3 | - TD-120 Missing cmakedefine for HAVE_GET_CURRENT_DIR_NAME 4 | (thanks to Jörg Krause, Ben Boeckel). 5 | - TI-91 Timewarrior does not compile on DragonFly 6 | (thanks to Michael Neumann). 7 | - TW-1845 Cygwin build fails, missing get_current_dir_name 8 | (thanks to hosaka). 9 | - TW-1936 Tweak tests to have fuller TAP compliance 10 | (thanks to Paul J. Fenwick 11 | - CL-3 clog; nested include files 12 | (thanks to David Patrick). 13 | 14 | ------ current release --------------------------- 15 | 16 | 1.3.0 (2016-06-27) 98b53f0142ce85725d83a6fad0091f7f92b6269d 17 | - CL-10 The suppress context doesn't suppress the newline character 18 | (thanks to Cory Donnelly). 19 | - CL-12 Overlapping rules are not respected 20 | (thanks to P4). 21 | - Converted to C++11. 22 | - Improved compliance to CppCoreGuidelines. 23 | - Integrated libshared.git, code-sharing across Taskwarrior products. 24 | 25 | ------ old releases ------------------------------ 26 | 27 | 1.2.1 (2015-10-12) 33c912809e13f24801d273614f9d6dfc14b1ef41 28 | - CL-8 clog 1.2.0 fails to build 29 | Thanks to Rainer Müller 30 | 31 | 1.2.0 (2015-09-26) 48719a21fda4e29113ce8b1aa29a568f073f0e65 32 | - TW-1296 make test/run_all exit with non-zero code if a test fail (thanks to 33 | Jakub Wilk). 34 | - Fixed bug whereby blank input lines always got eaten (thanks to vkrishn). 35 | - Updated URLs. 36 | 37 | 1.1.0 (2013-09-10) 3feae394c0407954e7eb345ffdcc29ae09e23ba6 38 | - Supports action 'blank' that adds a preceding and following blank line to a 39 | match. 40 | 41 | 1.0.0 (2012-08-05) 42 | - Supports regular expressions and text strings. 43 | - Supports multiple rulesets in different sections of ~/.clogrc. 44 | - Supports adding date- or time-stamps to each line. 45 | - Supports colorizing lines or patterns. 46 | - Supports suppression of lines. 47 | - Has clog.1 man page. 48 | - Uses cmake build system. 49 | 50 | Project started September 22, 2010. 51 | 52 | ------ start ----------------------------------- 53 | 54 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | Installation Instructions 2 | ------------------------- 3 | 4 | Please follow the instructions below to build clog with cmake. 5 | 6 | 7 | Pre-requisites 8 | -------------- 9 | 10 | You will need the 'cmake' build system installed in order to build clog 11 | from source. 12 | 13 | More information on cmake can be obtained at http://cmake.org 14 | 15 | 16 | Basic Installation 17 | ------------------ 18 | 19 | Briefly, these shell commands will unpack, build and install clog: 20 | 21 | $ tar xzf clog-1.4.0.tar.gz [1] 22 | $ cd clog-1.4.0 [2] 23 | $ cmake . [3] 24 | $ make [4] 25 | $ make test [5] 26 | $ sudo make install [6] 27 | $ cd .. ; rm -r clog-1.4.0 [7] 28 | 29 | These commands are explained below: 30 | 31 | 1. Unpacks the source tarball. This creates the directory clog-1.4.0, 32 | containing all the code. 33 | 34 | 2. Change directory to the root of the distribution. 35 | 36 | 3. Invokes cmake to scan for dependencies and machine-specific details, then 37 | generate the makefiles. This may take a minute. 38 | 39 | 4. Builds clog. This may take a minute. 40 | 41 | 5. Optional step that runs the unit tests. On completion, will report the 42 | number of passing and failing tests. There should be zero failing tests. 43 | If you see failing tests, please report this. 44 | 45 | 6. Installs the program, documentation and other data files. 46 | 47 | 7. Removes the temporary directory. 48 | 49 | 50 | Uninstallation 51 | -------------- 52 | 53 | To uninstall clog, you need the Makefiles, so if you deleted them in step 7 54 | above, they must first be regenerated by following steps [1], [2] and [3]. Then 55 | simply run: 56 | 57 | $ sudo make uninstall TODO TODO TODO -- cmake doesn't include uninstall 58 | 59 | 60 | Clog Build Notes 61 | ---------------- 62 | 63 | Clog has dependencies that are detected by cmake in almost all cases, but there 64 | are situations and operating systems that mean you will need to offer a little help. 65 | 66 | If clog will not build on your system, first take a look at the Operating System 67 | notes below. If this doesn't help, then go to the Troubleshooting section, 68 | which includes instructions on how to contact us for help. 69 | 70 | 71 | 72 | Operating System Notes 73 | ---------------------- 74 | 75 | ... 76 | 77 | 78 | Troubleshooting 79 | --------------- 80 | 81 | ... 82 | 83 | 84 | --- 85 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright 2010 - 2017, Göteborg Bit Factory. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | 22 | http://www.opensource.org/licenses/mit-license.php 23 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | 2 | New Features in clog 1.4.0 3 | 4 | - Allows nested 'include ' statements in ~/.flodrc. 5 | 6 | Please refer to the ChangeLog file for full details. 7 | 8 | --- 9 | 10 | Clog has been built and tested on the following configurations: 11 | 12 | * macOS 13 | * Fedora 14 | * Ubuntu 15 | * Debian 16 | * Arch 17 | * FreeBSD 18 | * Cygwin 19 | 20 | --- 21 | 22 | While Clog has undergone testing, bugs are sure to remain. If you 23 | encounter a bug, please enter a new issue at: 24 | 25 | https://bug.tasktools.org 26 | 27 | Or just send a message to: 28 | 29 | support@taskwarrior.org 30 | 31 | Thank you. 32 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | What is clog? 2 | ============= 3 | 4 | Clog is a filter that colorizes log files. Have you ever done something like 5 | this: 6 | 7 | $ tail -f /var/log/something.log 8 | 9 | Then left that running in a terminal, while you debug or run some program? Were 10 | you able to spot important information as it scrolled by? Depending on the 11 | volume of data scrolling by, it can be difficult to spot important information. 12 | 13 | Clog is a filter, that you run like this: 14 | 15 | $ tail -f /var/log/something.log | clog something 16 | 17 | The 'something' argument to clog is a section, which refers to a section in 18 | ~/.clogrc with a distinct set of rules. Those rules list patterns to look for, 19 | and actions to take when the pattern is matched by a line. A typical example 20 | might be that you want to highlight any line that contains the pattern 'severe'. 21 | This entry in ~/clogrc achieves this: 22 | 23 | something rule /severe/ --> bold line 24 | 25 | This rule is in the 'something' section, the pattern is 'severe', and the action 26 | taken is to embolden the whole line. Pattern 'severe' should probably be more 27 | restrictive, because it will also match words like 'persevered', but that is 28 | optional. 29 | 30 | Any color can be used, in both the 16- and 256-color space. Some examples are: 31 | 32 | bold 33 | underline 34 | bold blue 35 | underline on green 36 | black on white 37 | bold red on bright white 38 | rgb200 on grey4 39 | 40 | Instead of coloring the whole line, specifying 'match' instead will only color 41 | the parts of the line that match. 42 | 43 | The format of the rules is: 44 | 45 |
rule // --> 46 | 47 | The section is simply a way to allow multiple rules sets, so that one .clogrc 48 | file can serve multiple uses. The pattern is any supported Standard C Library 49 | regular expression. Action must be one of 'line', 'match', 'suppress' or 50 | 'blank'. 51 | 52 | Rules are processed in order, from top to bottom. This means that a rule defined 53 | lower in the rc file gets to apply it's color later, and therefore 'on top of' 54 | that of an earlier rule. 55 | 56 | Note that there is a default section, called 'default'. Putting rules in the 57 | default section means that you do not need to specify a section on the command 58 | line. Multiple sections may be specified, and the rules are combined. 59 | 60 | --- 61 | -------------------------------------------------------------------------------- /cmake.h.in: -------------------------------------------------------------------------------- 1 | /* cmake.h.in. Creates cmake.h during a build */ 2 | 3 | /* Package information */ 4 | #define PACKAGE "${PACKAGE}" 5 | #define VERSION "${VERSION}" 6 | #define PACKAGE_BUGREPORT "${PACKAGE_BUGREPORT}" 7 | #define PACKAGE_NAME "${PACKAGE_NAME}" 8 | #define PACKAGE_TARNAME "${PACKAGE_TARNAME}" 9 | #define PACKAGE_VERSION "${PACKAGE_VERSION}" 10 | #define PACKAGE_STRING "${PACKAGE_STRING}" 11 | 12 | /* git information */ 13 | #cmakedefine HAVE_COMMIT 14 | 15 | /* Compiling platform */ 16 | #cmakedefine LINUX 17 | #cmakedefine DARWIN 18 | #cmakedefine KFREEBSD 19 | #cmakedefine FREEBSD 20 | #cmakedefine OPENBSD 21 | #cmakedefine NETBSD 22 | #cmakedefine DRAGONFLY 23 | #cmakedefine SOLARIS 24 | #cmakedefine GNUHURD 25 | #cmakedefine CYGWIN 26 | #cmakedefine UNKNOWN 27 | 28 | /* Found tm.tm_gmtoff struct member */ 29 | #cmakedefine HAVE_TM_GMTOFF 30 | 31 | /* Found st.st_birthtime struct member */ 32 | #cmakedefine HAVE_ST_BIRTHTIME 33 | 34 | /* Functions */ 35 | #cmakedefine HAVE_GET_CURRENT_DIR_NAME 36 | #cmakedefine HAVE_TIMEGM 37 | #cmakedefine HAVE_UUID_UNPARSE_LOWER 38 | 39 | -------------------------------------------------------------------------------- /cmake/CXXSniffer.cmake: -------------------------------------------------------------------------------- 1 | message ("-- Configuring C++11") 2 | message ("-- System: ${CMAKE_SYSTEM_NAME}") 3 | 4 | include (CheckCXXCompilerFlag) 5 | 6 | # NOTE: Phase out -std=gnu++0x and --std=c++0x as soon as realistically possible. 7 | CHECK_CXX_COMPILER_FLAG("-std=c++11" _HAS_CXX11) 8 | CHECK_CXX_COMPILER_FLAG("-std=c++0x" _HAS_CXX0X) 9 | CHECK_CXX_COMPILER_FLAG("-std=gnu++0x" _HAS_GNU0X) 10 | 11 | if (_HAS_CXX11) 12 | set (_CXX11_FLAGS "-std=c++11") 13 | elseif (_HAS_CXX0X) 14 | message (WARNING "Enabling -std=c++0x draft compile flag. Your compiler does not support the standard '-std=c++11' option. Consider upgrading.") 15 | set (_CXX11_FLAGS "-std=c++0x") 16 | elseif (_HAS_GNU0X) 17 | message (WARNING "Enabling -std=gnu++0x draft compile flag. Your compiler does not support the standard '-std=c++11' option. Consider upgrading.") 18 | set (_CXX11_FLAGS "-std=gnu++0x") 19 | else (_HAS_CXX11) 20 | message (FATAL_ERROR "C++11 support missing. Try upgrading your C++ compiler. If you have a good reason for using an outdated compiler, please let us know at support@gothenburgbitfactory.org.") 21 | endif (_HAS_CXX11) 22 | 23 | if (${CMAKE_SYSTEM_NAME} MATCHES "Linux") 24 | set (LINUX true) 25 | elseif (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") 26 | set (DARWIN true) 27 | set (_CXX11_FLAGS "${_CXX11_FLAGS} -stdlib=libc++") 28 | elseif (${CMAKE_SYSTEM_NAME} MATCHES "kFreeBSD") 29 | set (KFREEBSD true) 30 | elseif (${CMAKE_SYSTEM_NAME} MATCHES "FreeBSD") 31 | set (FREEBSD true) 32 | elseif (${CMAKE_SYSTEM_NAME} MATCHES "OpenBSD") 33 | set (OPENBSD true) 34 | elseif (${CMAKE_SYSTEM_NAME} MATCHES "NetBSD") 35 | set (NETBSD true) 36 | elseif (${CMAKE_SYSTEM_NAME} MATCHES "DragonFly") 37 | set (DRAGONFLY true) 38 | elseif (${CMAKE_SYSTEM_NAME} MATCHES "SunOS") 39 | set (SOLARIS true) 40 | elseif (${CMAKE_SYSTEM_NAME} STREQUAL "GNU") 41 | set (GNUHURD true) 42 | elseif (${CMAKE_SYSTEM_NAME} STREQUAL "CYGWIN") 43 | set (CYGWIN true) 44 | # NOTE: Not setting -std=gnu++0x leads to compile errors even with 45 | # GCC 4.8.3, and debugging those leads to insanity. Adding this 46 | # workaround instead of fixing Cygwin. 47 | set (_CXX11_FLAGS "-std=gnu++0x") 48 | else (${CMAKE_SYSTEM_NAME} MATCHES "Linux") 49 | set (UNKNOWN true) 50 | endif (${CMAKE_SYSTEM_NAME} MATCHES "Linux") 51 | 52 | set (CMAKE_CXX_FLAGS "${_CXX11_FLAGS} ${CMAKE_CXX_FLAGS}") 53 | set (CMAKE_CXX_FLAGS "-Wall -Wextra -Wsign-compare -Wreturn-type ${CMAKE_CXX_FLAGS}") 54 | -------------------------------------------------------------------------------- /doc/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 2.8) 2 | message ("-- Configuring man pages") 3 | set (man_FILES clog.1) 4 | foreach (man_FILE ${man_FILES}) 5 | configure_file ( 6 | man/${man_FILE}.in 7 | man/${man_FILE}) 8 | endforeach (man_FILE) 9 | 10 | install (DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/man/ DESTINATION share/man/man1 11 | FILES_MATCHING PATTERN "*.1") 12 | -------------------------------------------------------------------------------- /doc/demo/0.txt: -------------------------------------------------------------------------------- 1 | We start with a fragment of text, from the opening scene of Hamlet. This is 2 | just an example, but will illustrate how clog can be applied to log files. 3 | 4 | $ cat hamlet 5 | FRANCISCO at his post. Enter to him BERNARDO 6 | BERNARDO Whos there? 7 | FRANCISCO Nay, answer me: stand, and unfold yourself. 8 | BERNARDO Long live the king. 9 | FRANCISCO Bernardo? 10 | BERNARDO He. 11 | FRANCISCO You come most carefully upon your hour. 12 | BERNARDO Tis now struck twelve, get thee to bed, Francisco. 13 | FRANCISCO For this relief much thanks: tis bitter cold, And I am sick at heart. 14 | BERNARDO Have you had quiet guard? 15 | FRANCISCO Not a mouse stirring. 16 | BERNARDO Well, good night. If you do meet Horatio and Marcellus, The rivals of my watch, bid them make haste. 17 | FRANCISCO I think I hear them. Stand, ho. Whos there? 18 | HORATIO Friends to this ground. 19 | MARCELLUS And liegemen to the Dane. 20 | FRANCISCO Give you good night. 21 | MARCELLUS O, farewell, honest soldier: Who hath relieved you? 22 | FRANCISCO Bernardo has my place. Give you good night. 23 | MARCELLUS Holla. Bernardo. 24 | BERNARDO Say, What, is Horatio there? 25 | HORATIO A piece of him. 26 | 27 | By default, clog is a pass-through filter that does nothing: 28 | 29 | $ clog < hamlet > foo 30 | $ diff hamlet foo 31 | $ 32 | 33 | -------------------------------------------------------------------------------- /doc/demo/1.txt: -------------------------------------------------------------------------------- 1 | Clog has some basic features, for example, it can prepend timestamps to text: 2 | 3 | $ head -5 hamlet | clog --date 4 | 2013-04-11 FRANCISCO at his post. Enter to him BERNARDO 5 | 2013-04-11 BERNARDO Whos there? 6 | 2013-04-11 FRANCISCO Nay, answer me: stand, and unfold yourself. 7 | 2013-04-11 BERNARDO Long live the king. 8 | 2013-04-11 FRANCISCO Bernardo? 9 | 10 | $ head -5 hamlet | clog --time 11 | 21:20:17 FRANCISCO at his post. Enter to him BERNARDO 12 | 21:20:17 BERNARDO Whos there? 13 | 21:20:17 FRANCISCO Nay, answer me: stand, and unfold yourself. 14 | 21:20:17 BERNARDO Long live the king. 15 | 21:20:17 FRANCISCO Bernardo? 16 | 17 | $ head -5 hamlet | clog --date --time 18 | 2013-04-11 21:20:22 FRANCISCO at his post. Enter to him BERNARDO 19 | 2013-04-11 21:20:22 BERNARDO Whos there? 20 | 2013-04-11 21:20:22 FRANCISCO Nay, answer me: stand, and unfold yourself. 21 | 2013-04-11 21:20:22 BERNARDO Long live the king. 22 | 2013-04-11 21:20:22 FRANCISCO Bernardo? 23 | 24 | -------------------------------------------------------------------------------- /doc/demo/2.txt: -------------------------------------------------------------------------------- 1 | Now we add a rule, to colorize some text: 2 | 3 | $ echo 'default rule "HORATIO" --> red match' > ~/.clogrc 4 | 5 | This rule says that if the text "HORATIO" is found, then the matching text is 6 | colored red. The 'default' keyword is a rule section, which allows multiple 7 | rule sets. 8 | 9 | $ clog < hamlet 10 | FRANCISCO at his post. Enter to him BERNARDO 11 | BERNARDO Whos there? 12 | FRANCISCO Nay, answer me: stand, and unfold yourself. 13 | BERNARDO Long live the king. 14 | FRANCISCO Bernardo? 15 | BERNARDO He. 16 | FRANCISCO You come most carefully upon your hour. 17 | BERNARDO Tis now struck twelve, get thee to bed, Francisco. 18 | FRANCISCO For this relief much thanks: tis bitter cold, And I am sick at heart. 19 | BERNARDO Have you had quiet guard? 20 | FRANCISCO Not a mouse stirring. 21 | BERNARDO Well, good night. If you do meet Horatio and Marcellus, The rivals of my watch, bid them make haste. 22 | FRANCISCO I think I hear them. Stand, ho. Whos there? 23 | HORATIO Friends to this ground. 24 | MARCELLUS And liegemen to the Dane. 25 | FRANCISCO Give you good night. 26 | MARCELLUS O, farewell, honest soldier: Who hath relieved you? 27 | FRANCISCO Bernardo has my place. Give you good night. 28 | MARCELLUS Holla. Bernardo. 29 | BERNARDO Say, What, is Horatio there? 30 | HORATIO A piece of him. 31 | 32 | -------------------------------------------------------------------------------- /doc/demo/3.txt: -------------------------------------------------------------------------------- 1 | Another rule: 2 | 3 | $ echo 'default rule "MARCELLUS" --> black on blue line' >> ~/.clogrc 4 | 5 | This rule says that if the text "MARCELLUS" is found, then the matching line is 6 | colored with a blue background. 7 | 8 | $ clog < hamlet 9 | FRANCISCO at his post. Enter to him BERNARDO 10 | BERNARDO Whos there? 11 | FRANCISCO Nay, answer me: stand, and unfold yourself. 12 | BERNARDO Long live the king. 13 | FRANCISCO Bernardo? 14 | BERNARDO He. 15 | FRANCISCO You come most carefully upon your hour. 16 | BERNARDO Tis now struck twelve, get thee to bed, Francisco. 17 | FRANCISCO For this relief much thanks: tis bitter cold, And I am sick at heart. 18 | BERNARDO Have you had quiet guard? 19 | FRANCISCO Not a mouse stirring. 20 | BERNARDO Well, good night. If you do meet Horatio and Marcellus, The rivals of my watch, bid them make haste. 21 | FRANCISCO I think I hear them. Stand, ho. Whos there? 22 | HORATIO Friends to this ground. 23 | MARCELLUS And liegemen to the Dane. 24 | FRANCISCO Give you good night. 25 | MARCELLUS O, farewell, honest soldier: Who hath relieved you? 26 | FRANCISCO Bernardo has my place. Give you good night. 27 | MARCELLUS Holla. Bernardo. 28 | BERNARDO Say, What, is Horatio there? 29 | HORATIO A piece of him. 30 | 31 | -------------------------------------------------------------------------------- /doc/demo/4.txt: -------------------------------------------------------------------------------- 1 | This rule will suppress the line, instead of coloring it: 2 | 3 | $ echo 'default rule "FRANCISCO" --> suppress' >> ~/.clogrc 4 | $ 5 | $ clog < hamlet 6 | BERNARDO Whos there? 7 | BERNARDO Long live the king. 8 | BERNARDO He. 9 | BERNARDO Tis now struck twelve, get thee to bed, Francisco. 10 | BERNARDO Have you had quiet guard? 11 | BERNARDO Well, good night. If you do meet Horatio and Marcellus, The rivals of my watch, bid them make haste. 12 | HORATIO Friends to this ground. 13 | MARCELLUS And liegemen to the Dane. 14 | MARCELLUS O, farewell, honest soldier: Who hath relieved you? 15 | MARCELLUS Holla. Bernardo. 16 | BERNARDO Say, What, is Horatio there? 17 | HORATIO A piece of him. 18 | -------------------------------------------------------------------------------- /doc/demo/5.txt: -------------------------------------------------------------------------------- 1 | So far, the rules have all involved text string matches. Clog also supports 2 | regular expressions, with a slightly different syntax: 3 | 4 | $ rm ~/.clogrc 5 | $ echo 'default rule /[Ww]ho/ --> red match' >> ~/.clogrc 6 | $ echo 'default rule /n...t/ --> blue match' >> ~/.clogrc 7 | 8 | This rules color 'Who' or 'who' red, and the word 'night' (or anything matching 9 | 'n...t') blue. 10 | 11 | $ clog < hamlet 12 | FRANCISCO at his post. Enter to him BERNARDO 13 | BERNARDO Whos there? 14 | FRANCISCO Nay, answer me: stand, and unfold yourself. 15 | BERNARDO Long live the king. 16 | FRANCISCO Bernardo? 17 | BERNARDO He. 18 | FRANCISCO You come most carefully upon your hour. 19 | BERNARDO Tis now struck twelve, get thee to bed, Francisco. 20 | FRANCISCO For this relief much thanks: tis bitter cold, And I am sick at heart. 21 | BERNARDO Have you had quiet guard? 22 | FRANCISCO Not a mouse stirring. 23 | BERNARDO Well, good night. If you do meet Horatio and Marcellus, The rivals of my watch, bid them make haste. 24 | FRANCISCO I think I hear them. Stand, ho. Whos there? 25 | HORATIO Friends to this ground. 26 | MARCELLUS And liegemen to the Dane. 27 | FRANCISCO Give you good night. 28 | MARCELLUS O, farewell, honest soldier: Who hath relieved you? 29 | FRANCISCO Bernardo has my place. Give you good night. 30 | MARCELLUS Holla. Bernardo. 31 | BERNARDO Say, What, is Horatio there? 32 | HORATIO A piece of him. 33 | 34 | -------------------------------------------------------------------------------- /doc/demo/6.txt: -------------------------------------------------------------------------------- 1 | The section name can be used to maintain several rule sets. Here is a new 2 | example using two rule sets: 3 | 4 | $ echo '# Shakespeare characters' >> ~/.clogrc 5 | $ echo 'Shakespeare rule "FRANCISCO" --> red match' >> ~/.clogrc 6 | $ echo 'Shakespeare rule "BERNARDO" --> blue match' >> ~/.clogrc 7 | $ echo 'Shakespeare rule "MARCELLUS" --> green match' >> ~/.clogrc 8 | $ echo 'Shakespeare rule "HORATIO" --> white match' >> ~/.clogrc 9 | $ 10 | $ echo '# Syslog severities' >> ~/.clogrc 11 | $ echo 'syslog rule /warn|debug/ --> yellow line' >> ~/.clogrc 12 | $ echo 'syslog rule /error|severe/ --> red line' >> ~/.clogrc 13 | $ echo 'syslog rule "critical" --> bold red line' >> ~/.clogrc 14 | $ echo 'syslog rule "ignore" --> suppress' >> ~/.clogrc 15 | 16 | Now we can invoke the default set from before, by not specifying a rule set: 17 | 18 | $ clog < hamlet 19 | ... 20 | 21 | Or we can select the Shakespeare set, by providing the rule set name as an 22 | argument: 23 | 24 | $ clog Shakespeare < hamlet 25 | ... 26 | 27 | We can specify multiple rule set names if we wish, but these do not include 28 | the default set: 29 | 30 | $ clog Shakespeare syslog < hamlet 31 | ... 32 | 33 | -------------------------------------------------------------------------------- /doc/demo/hamlet: -------------------------------------------------------------------------------- 1 | FRANCISCO at his post. Enter to him BERNARDO 2 | BERNARDO Whos there? 3 | FRANCISCO Nay, answer me: stand, and unfold yourself. 4 | BERNARDO Long live the king. 5 | FRANCISCO Bernardo? 6 | BERNARDO He. 7 | FRANCISCO You come most carefully upon your hour. 8 | BERNARDO Tis now struck twelve, get thee to bed, Francisco. 9 | FRANCISCO For this relief much thanks: tis bitter cold, And I am sick at heart. 10 | BERNARDO Have you had quiet guard? 11 | FRANCISCO Not a mouse stirring. 12 | BERNARDO Well, good night. If you do meet Horatio and Marcellus, The rivals of my watch, bid them make haste. 13 | FRANCISCO I think I hear them. Stand, ho. Whos there? 14 | HORATIO Friends to this ground. 15 | MARCELLUS And liegemen to the Dane. 16 | FRANCISCO Give you good night. 17 | MARCELLUS O, farewell, honest soldier: Who hath relieved you? 18 | FRANCISCO Bernardo has my place. Give you good night. 19 | MARCELLUS Holla. Bernardo. 20 | BERNARDO Say, What, is Horatio there? 21 | HORATIO A piece of him. 22 | -------------------------------------------------------------------------------- /doc/demo/intro.txt: -------------------------------------------------------------------------------- 1 | Introducing clog. 2 | 3 | Clog is a colorized log viewer. It can spot text on a line in a log file and 4 | apply color to it, a little like this: 5 | 6 | $ tail -f access.log | sed -e 's/ error / error<\/red> /g' 7 | 8 | This would spot the text ' error ' in access.log and mark up the matched text. 9 | This is what clog does. 10 | 11 | -------------------------------------------------------------------------------- /doc/man/.gitignore: -------------------------------------------------------------------------------- 1 | clog.1 2 | -------------------------------------------------------------------------------- /doc/man/clog.1.in: -------------------------------------------------------------------------------- 1 | .TH clog 1 2016-06-28 "${PACKAGE_STRING}" "User Manuals" 2 | 3 | .SH NAME 4 | clog \- colorized log filter 5 | 6 | .SH SYNOPSIS 7 | .B clog [] [
...] 8 | 9 | -h|--help Show this usage 10 | -v|--version Show this version 11 | -d|--date Prepend all lines with the current date 12 | -t|--time Prepend all lines with the current time 13 | -f|--file Override default ~/.clogrc 14 | 15 | .SH DESCRIPTION 16 | Clog is a filter command, and therefore copies its input to its output. But if 17 | lines in the input match certain patterns, actions are taken, which are defined 18 | as 'rules'. 19 | 20 | The rules are defined in ~/.clogrc as text patterns or regular expressions, and 21 | the actions are specified, to colorize either the line or matching pattern, or 22 | perhaps to suppress the line. 23 | 24 | If --date is specified the current date, YYYY-MM-DD, is prepended to all lines. 25 | 26 | If --time is specified the current time, HH:MM:SS, is prepended to all lines. 27 | 28 | If --file is specified, an alternate configuration rc file may be specified. 29 | Default is to ~/.clogrc 30 | 31 | One or more section arguments may be specified. If none are provided, 'default' 32 | is assumed. A section corresponds to a named rule set defined in ~/.clogrc. and 33 | allows the use of one .clogrc file to serve multiple different uses of clog. 34 | If more than one section is specified, the rules sets are combined, in the 35 | sequence found. 36 | 37 | .SH CONFIGURATION FILE AND OVERRIDE OPTIONS 38 | Clog reads its configuration from a file in the user's home directory: 39 | ~/.clogrc. 40 | 41 | The configuration file can include other configuration files, using this syntax: 42 | 43 | .RS 44 | include /path/to/other/file 45 | .br 46 | include ~/rules.txt 47 | .RE 48 | 49 | The format of the rules is: 50 | 51 | .RS 52 |
rule // --> 53 | .br 54 |
rule "" --> 55 | .RE 56 | 57 | If the pattern is surrounded by / characters, it is interpreted as a regular 58 | expression. If the pattern is surrounded by " characters, it is interpreted as 59 | a string fragment. 60 | 61 | The section is simply a way to allow multiple rules sets, so that one .clogrc 62 | file can serve multiple uses. The pattern may be any supported Standard C 63 | Library regular expression. Action must be one of 'line', 'match', 'suppress' 64 | or 'blank'. 65 | 66 | Note that there is a default section, called 'default'. Putting rules in the 67 | default section means that no section need be specified on the command line. 68 | 69 | Any color can be used, in both the 16- and 256-color space. Some examples are: 70 | 71 | .RS 72 | bold 73 | .br 74 | underline 75 | .br 76 | bold blue 77 | .br 78 | underline on green 79 | .br 80 | black on white 81 | .br 82 | bold red on bright white 83 | .br 84 | rgb200 on grey4 85 | .RE 86 | 87 | Instead of coloring the whole line, specifying 'match' instead will only color 88 | the parts of the line that match. 89 | 90 | .SH EXAMPLE Rulesets 91 | Here is an example ~/.clogrc file. 92 | 93 | .RS 94 | # Standard syslog entries. 95 | .br 96 | default rule /warn|debug/ --> yellow line 97 | .br 98 | default rule /error|severe/ --> red line 99 | .br 100 | default rule /critical/ --> bold red line 101 | .br 102 | default rule /critical/ --> blank 103 | .br 104 | default rule /ignore/ --> suppress 105 | .br 106 | 107 | .br 108 | # Apache access.log status codes 109 | .br 110 | apache rule / 2[0-9][0-9] / --> green match 111 | .br 112 | apache rule / 3[0-9][0-9] / --> yellow match 113 | .br 114 | apache rule / 4[0-9][0-9] / --> red match 115 | .br 116 | apache rule / 5[0-9][0-9] / --> bold red match 117 | .RE 118 | 119 | .SH "CREDITS & COPYRIGHTS" 120 | Copyright (C) 2006 \- 2017 P. Beckingham, F. Hernandez. 121 | .br 122 | Copyright (C) 2006 \- 2017 Goteborg Bit Factory. 123 | 124 | Clog is distributed under the MIT license. See 125 | http://www.opensource.org/licenses/mit-license.php for more information. 126 | 127 | .SH SEE ALSO 128 | For more information, see: 129 | 130 | .TP 131 | The official site at 132 | 133 | 134 | .TP 135 | The official code repository at 136 | 137 | 138 | .TP 139 | You can contact the project by emailing 140 | 141 | 142 | .SH REPORTING BUGS 143 | .TP 144 | Bugs in Taskwarrior may be reported to the issue-tracker at 145 | 146 | 147 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | clog 2 | libclog.a 3 | liblibshared.a 4 | debug 5 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8) 2 | include_directories (${CMAKE_SOURCE_DIR} 3 | ${CMAKE_SOURCE_DIR}/src 4 | ${CMAKE_SOURCE_DIR}/src/libshared/src 5 | ${CLOG_INCLUDE_DIRS}) 6 | 7 | set (clog_SRCS clog.cpp rules.cpp Rule.cpp Rule.h) 8 | 9 | set (libshared_SRCS 10 | libshared/src/Color.cpp libshared/src/Color.h 11 | libshared/src/Composite.cpp libshared/src/Composite.h 12 | libshared/src/FS.cpp libshared/src/FS.h 13 | libshared/src/Pig.cpp libshared/src/Pig.h 14 | libshared/src/RX.cpp libshared/src/RX.h 15 | libshared/src/format.cpp libshared/src/format.h 16 | libshared/src/shared.cpp libshared/src/shared.h 17 | libshared/src/unicode.cpp libshared/src/unicode.h 18 | libshared/src/utf8.cpp libshared/src/utf8.h 19 | libshared/src/wcwidth6.cpp) 20 | 21 | add_library (clog STATIC ${clog_SRCS}) 22 | add_library (libshared STATIC ${libshared_SRCS}) 23 | add_executable (clog_executable clog.cpp) 24 | 25 | target_link_libraries (clog_executable clog libshared ${CLOG_LIBRARIES}) 26 | 27 | set_property (TARGET clog_executable PROPERTY OUTPUT_NAME "clog") 28 | 29 | install (TARGETS clog_executable DESTINATION bin) 30 | 31 | -------------------------------------------------------------------------------- /src/Rule.cpp: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2010 - 2017, Paul Beckingham, Federico Hernandez. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included 13 | // in all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | // 23 | // http://www.opensource.org/licenses/mit-license.php 24 | // 25 | //////////////////////////////////////////////////////////////////////////////// 26 | 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | //////////////////////////////////////////////////////////////////////////////// 34 | //
rule // --> 35 | // taskd rule /code:"2.."/ --> green line 36 | Rule::Rule (const std::string& line) 37 | { 38 | _fragment = ""; 39 | 40 | Pig pig (line); 41 | pig.skipWS (); 42 | 43 | std::string pattern; 44 | if (pig.getUntilWS (_section) && 45 | pig.skipWS () && 46 | pig.skipLiteral ("rule") && 47 | pig.skipWS ()) 48 | { 49 | //
rule // 50 | if (pig.getQuoted ('/', pattern) && 51 | pig.skipWS () && 52 | pig.skipLiteral ("-->")) 53 | { 54 | pig.skipWS (); 55 | 56 | std::string rest; 57 | pig.getRemainder (rest); 58 | 59 | std::string color_name; 60 | std::vector words = split (rest, ' '); 61 | for (auto& word : words) 62 | { 63 | if (word.length ()) 64 | { 65 | if (word == "line") _context = word; 66 | else if (word == "match") _context = word; 67 | else if (word == "suppress") _context = word; 68 | else if (word == "blank") _context = word; 69 | else 70 | { 71 | if (color_name.length ()) 72 | color_name += " "; 73 | 74 | color_name += word; 75 | } 76 | } 77 | } 78 | 79 | _color = Color (color_name); 80 | 81 | // Now for "match" context patterns, add an enclosing ( ... ) if not 82 | // already present. 83 | if (_context == "match") 84 | if (pattern.find ('(') == std::string::npos) 85 | pattern = "(" + pattern + ")"; 86 | 87 | _rx = RX (pattern, true); 88 | return; 89 | } 90 | 91 | //
rule "" 92 | else if (pig.getQuoted ('"', pattern) && 93 | pig.skipWS () && 94 | pig.skipLiteral ("-->")) 95 | { 96 | pig.skipWS (); 97 | 98 | std::string rest; 99 | pig.getRemainder (rest); 100 | 101 | std::string color_name; 102 | auto words = split (rest); 103 | for (auto& word : words) 104 | { 105 | if (word.length ()) 106 | { 107 | if (word == "line") _context = word; 108 | else if (word == "match") _context = word; 109 | else if (word == "suppress") _context = word; 110 | else if (word == "blank") _context = word; 111 | // TODO Support _context "datetime", "time" 112 | else 113 | { 114 | if (color_name.length ()) 115 | color_name += " "; 116 | 117 | color_name += word; 118 | } 119 | } 120 | } 121 | 122 | _color = Color (color_name); 123 | _fragment = pattern; 124 | return; 125 | } 126 | } 127 | 128 | // Indicates that 'line' was not a rule def, but a blank line or similar. 129 | throw int (1); 130 | } 131 | 132 | //////////////////////////////////////////////////////////////////////////////// 133 | // There are two kinds of matching: 134 | // - regex (when _fragment is "") 135 | // - substring (when _fragment is not "") 136 | // 137 | // There are several corresponding actions: 138 | // - suppress Eats the whole line, including \n 139 | // - line Colorizes the line 140 | // - match Colorizes the matching part 141 | // - blank Adds a blank line before and after 142 | // 143 | bool Rule::apply (Composite& composite, bool& blanks, const std::string& section, const std::string& line) 144 | { 145 | if (_section == section) 146 | { 147 | if (_context == "suppress") 148 | { 149 | if (_fragment != "") 150 | { 151 | if (line.find (_fragment) != std::string::npos) 152 | { 153 | composite.clear (); 154 | return true; 155 | } 156 | } 157 | else 158 | { 159 | if (_rx.match (line)) 160 | { 161 | composite.clear (); 162 | return true; 163 | } 164 | } 165 | } 166 | 167 | else if (_context == "line") 168 | { 169 | if (_fragment != "") 170 | { 171 | if (line.find (_fragment) != std::string::npos) 172 | { 173 | composite.add (line, 0, _color); 174 | return true; 175 | } 176 | } 177 | else 178 | { 179 | if (_rx.match (line)) 180 | { 181 | composite.add (line, 0, _color); 182 | return true; 183 | } 184 | } 185 | } 186 | 187 | else if (_context == "match") 188 | { 189 | if (_fragment != "") 190 | { 191 | bool found = false; 192 | auto pos = line.find (_fragment); 193 | while (pos != std::string::npos) 194 | { 195 | composite.add (line.substr (pos, _fragment.length ()), pos, _color); 196 | pos = line.find (_fragment, pos + 1); 197 | found = true; 198 | } 199 | 200 | if (found) 201 | return true; 202 | } 203 | else 204 | { 205 | std::vector start; 206 | std::vector end; 207 | if (_rx.match (start, end, line)) 208 | { 209 | for (unsigned int i = 0; i < start.size (); ++i) 210 | composite.add (line.substr (start[i], end[i] - start[i]), start[i], _color); 211 | 212 | return true; 213 | } 214 | } 215 | } 216 | 217 | else if (_context == "blank") 218 | { 219 | if (_fragment != "") 220 | { 221 | if (line.find (_fragment) != std::string::npos) 222 | { 223 | blanks = true; 224 | return true; 225 | } 226 | } 227 | else 228 | { 229 | if (_rx.match (line)) 230 | { 231 | blanks = true; 232 | return true; 233 | } 234 | } 235 | } 236 | } 237 | 238 | return false; 239 | } 240 | 241 | //////////////////////////////////////////////////////////////////////////////// 242 | -------------------------------------------------------------------------------- /src/Rule.h: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2010 - 2017, Paul Beckingham, Federico Hernandez. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included 13 | // in all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | // 23 | // http://www.opensource.org/licenses/mit-license.php 24 | // 25 | //////////////////////////////////////////////////////////////////////////////// 26 | 27 | #ifndef INCLUDED_RULE 28 | #define INCLUDED_RULE 29 | 30 | #include 31 | #include 32 | #include 33 | #include 34 | 35 | class Rule 36 | { 37 | public: 38 | explicit Rule (const std::string&); 39 | bool apply (Composite&, bool&, const std::string&, const std::string&); 40 | 41 | public: 42 | std::string _section {}; 43 | Color _color {}; 44 | std::string _context {}; 45 | RX _rx {}; // Regex for rule 46 | std::string _fragment {}; // String pattern for rule (not regex) 47 | }; 48 | 49 | #endif 50 | //////////////////////////////////////////////////////////////////////////////// 51 | -------------------------------------------------------------------------------- /src/clog.cpp: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2010 - 2017, Paul Beckingham, Federico Hernandez. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included 13 | // in all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | // 23 | // http://www.opensource.org/licenses/mit-license.php 24 | // 25 | //////////////////////////////////////////////////////////////////////////////// 26 | 27 | #include 28 | #include 29 | // If is included, put it after , because it includes 30 | // , and therefore would ignore the _WITH_GETLINE. 31 | #ifdef FREEBSD 32 | #define _WITH_GETLINE 33 | #endif 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | 47 | extern bool loadRules (const std::string&, std::vector &); 48 | 49 | //////////////////////////////////////////////////////////////////////////////// 50 | // Applies all the rules in all the sections specified. 51 | // Note that processing does not stop after the first rule match, it keeps going. 52 | void applyRules ( 53 | Composite& composite, 54 | bool& blanks, 55 | std::vector & rules, 56 | const std::vector & sections, 57 | const std::string& line) 58 | { 59 | composite.add (line, 0, {0}); 60 | 61 | for (const auto& section : sections) 62 | for (auto& rule : rules) 63 | rule.apply (composite, blanks, section, line); 64 | } 65 | 66 | //////////////////////////////////////////////////////////////////////////////// 67 | int main (int argc, char** argv) 68 | { 69 | int status = 0; 70 | 71 | try 72 | { 73 | // Locate $HOME. 74 | struct passwd* pw = getpwuid (getuid ()); 75 | if (!pw) 76 | throw std::string ("Could not determine the home directory."); 77 | 78 | // Assume ~/.clogrc 79 | std::string rcFile = pw->pw_dir; 80 | rcFile += "/.clogrc"; 81 | 82 | // Process arguments. 83 | std::vector sections; 84 | bool prepend_date = false; 85 | bool prepend_time = false; 86 | 87 | for (int i = 1; i < argc; ++i) 88 | { 89 | if (! strcmp (argv[i], "-h") || 90 | ! strcmp (argv[i], "--help")) 91 | { 92 | std::cout << '\n' 93 | << "Usage: clog [] [
...]\n" 94 | << '\n' 95 | << " -h|--help Show this usage\n" 96 | << " -v|--version Show this version\n" 97 | << " -d|--date Prepend all lines with the current date\n" 98 | << " -t|--time Prepend all lines with the current time\n" 99 | << " -f|--file Override default ~/.clogrc\n" 100 | << '\n'; 101 | return status; 102 | } 103 | 104 | else if (! strcmp (argv[i], "-v") || 105 | ! strcmp (argv[i], "--version")) 106 | { 107 | std::cout << "\n" 108 | << PACKAGE_STRING 109 | << " built for " 110 | << osName () 111 | << "\n" 112 | << "Copyright (C) 2010 - 2017 Göteborg Bit Factory\n" 113 | << "\n" 114 | << "Clog may be copied only under the terms of the MIT " 115 | "license, which may be found in the source kit.\n" 116 | << "\n" 117 | << "Documentation for clog can be found using 'man clog' " 118 | "or at http://tasktools.org/projects/clog.html\n" 119 | << "\n"; 120 | return status; 121 | } 122 | 123 | else if (! strcmp (argv[i], "-d") || 124 | ! strcmp (argv[i], "--date")) 125 | { 126 | prepend_date = true; 127 | } 128 | 129 | else if (! strcmp (argv[i], "-t") || 130 | ! strcmp (argv[i], "--time")) 131 | { 132 | prepend_time = true; 133 | } 134 | 135 | else if (argc > i + 1 && 136 | (! strcmp (argv[i], "-f") || 137 | ! strcmp (argv[i], "--file"))) 138 | { 139 | rcFile = argv[++i]; 140 | } 141 | 142 | else 143 | { 144 | sections.push_back (argv[i]); 145 | } 146 | } 147 | 148 | // Use a default section if one was not specified. 149 | if (sections.size () == 0) 150 | sections.push_back ("default"); 151 | 152 | // Read rc file. 153 | std::vector rules; 154 | if (loadRules (rcFile, rules)) 155 | { 156 | Composite composite; 157 | 158 | // Main loop: read line, apply rules, write line. 159 | std::string line; 160 | while (getline (std::cin, line)) // Strips \n 161 | { 162 | auto length = line.length (); 163 | bool blanks = false; 164 | applyRules (composite, blanks, rules, sections, line); 165 | 166 | if (blanks) 167 | std::cout << "\n"; 168 | 169 | auto output = composite.str (); 170 | if (output.length () || output.length () == length) 171 | { 172 | if (prepend_date || prepend_time) 173 | { 174 | time_t current; 175 | time (¤t); 176 | struct tm* t = localtime (¤t); 177 | 178 | if (prepend_date) 179 | std::cout << t->tm_year + 1900 << '-' 180 | << std::setw (2) << std::setfill ('0') << t->tm_mon + 1 << '-' 181 | << std::setw (2) << std::setfill ('0') << t->tm_mday << ' '; 182 | 183 | if (prepend_time) 184 | std::cout << std::setw (2) << std::setfill ('0') << t->tm_hour << ':' 185 | << std::setw (2) << std::setfill ('0') << t->tm_min << ':' 186 | << std::setw (2) << std::setfill ('0') << t->tm_sec << ' '; 187 | } 188 | 189 | std::cout << output << "\n"; 190 | } 191 | 192 | if (blanks) 193 | std::cout << "\n"; 194 | 195 | composite.clear (); 196 | } 197 | } 198 | else 199 | { 200 | std::cout << "Cannot open " << rcFile << "\n" 201 | << "See 'man clog' for details and a sample file.\n"; 202 | status = -1; 203 | } 204 | } 205 | 206 | catch (std::string& error) 207 | { 208 | std::cout << error << "\n"; 209 | return -1; 210 | } 211 | 212 | catch (...) 213 | { 214 | std::cout << "Unknown error\n"; 215 | return -2; 216 | } 217 | 218 | return status; 219 | } 220 | 221 | //////////////////////////////////////////////////////////////////////////////// 222 | -------------------------------------------------------------------------------- /src/rules.cpp: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2010 - 2017, Paul Beckingham, Federico Hernandez. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included 13 | // in all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | // 23 | // http://www.opensource.org/licenses/mit-license.php 24 | // 25 | //////////////////////////////////////////////////////////////////////////////// 26 | 27 | #include 28 | #include 29 | #include 30 | #include 31 | // If is included, put it after , because it includes 32 | // , and therefore would ignore the _WITH_GETLINE. 33 | #ifdef FREEBSD 34 | #define _WITH_GETLINE 35 | #endif 36 | #include 37 | #include 38 | #include 39 | #include 40 | 41 | //////////////////////////////////////////////////////////////////////////////// 42 | // - Read rc file 43 | // - Strip comments 44 | // - Parse rules 45 | // 46 | // Note that it is an error to not have an rc file. 47 | bool loadRules (const std::string& file, std::vector & rules) 48 | { 49 | std::ifstream rc (file.c_str ()); 50 | if (rc.good ()) 51 | { 52 | std::string::size_type comment; 53 | std::string line; 54 | while (std::getline (rc, line)) // Strips \n 55 | { 56 | // Remove comments. 57 | if ((comment = line.find ('#')) != std::string::npos) 58 | line.resize (comment); 59 | 60 | // Process each non-trivial line as a rule. 61 | if (line.length () > 1) 62 | { 63 | auto words = split (line); 64 | if (words.size () == 2 && 65 | words[0] == "include") 66 | { 67 | // File::File expands relative paths, and ~user. 68 | File f (words[1]); 69 | if (! loadRules (f._data, rules)) 70 | return false; 71 | } 72 | else 73 | { 74 | try 75 | { 76 | rules.push_back (Rule (line)); 77 | } 78 | catch (int) 79 | { 80 | // Deliberately ignored - error handling. 81 | } 82 | } 83 | } 84 | } 85 | 86 | rc.close (); 87 | return true; 88 | } 89 | 90 | return false; 91 | } 92 | 93 | //////////////////////////////////////////////////////////////////////////////// 94 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | all.log 2 | *.pyc 3 | rule.t 4 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 2.8) 2 | if(POLICY CMP0037) 3 | cmake_policy(SET CMP0037 OLD) 4 | endif() 5 | 6 | include_directories (${CMAKE_SOURCE_DIR} 7 | ${CMAKE_SOURCE_DIR}/src 8 | ${CMAKE_SOURCE_DIR}/src/libshared/src 9 | ${CMAKE_SOURCE_DIR}/test) 10 | 11 | include_directories (${CMAKE_INSTALL_PREFIX}/include) 12 | link_directories(${CMAKE_INSTALL_PREFIX}/lib) 13 | 14 | set (test_SRCS rule.t) 15 | 16 | add_custom_target (test ./run_all --verbose 17 | DEPENDS ${test_SRCS} 18 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/test) 19 | 20 | foreach (src_FILE ${test_SRCS}) 21 | add_executable (${src_FILE} "${src_FILE}.cpp" test.cpp) 22 | target_link_libraries (${src_FILE} clog libshared ${CLOG_LIBRARIES}) 23 | endforeach (src_FILE) 24 | 25 | configure_file(run_all run_all COPYONLY) 26 | configure_file(problems problems COPYONLY) 27 | 28 | -------------------------------------------------------------------------------- /test/basetest/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .testing import TestCase 4 | from .clog import Clog 5 | 6 | # flake8:noqa 7 | # vim: ai sts=4 et sw=4 8 | -------------------------------------------------------------------------------- /test/basetest/clog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import atexit 4 | import json 5 | import os 6 | import shlex 7 | import shutil 8 | import tempfile 9 | import unittest 10 | from .exceptions import CommandError 11 | from .utils import run_cmd_wait, run_cmd_wait_nofail, which, clog_binary_location, DEFAULT_EXTENSION_PATH 12 | from .compat import STRING_TYPE 13 | 14 | 15 | class Clog(object): 16 | """Manage a Clog instance 17 | 18 | A temporary folder is used as data store of clog. 19 | 20 | A clog client should not be used after being destroyed. 21 | """ 22 | DEFAULT_CLOG = clog_binary_location() 23 | 24 | def __init__(self, clog=DEFAULT_CLOG): 25 | """Initialize a clog (client). 26 | The program runs in a temporary folder. 27 | 28 | :arg clog: Clog binary to use as client (defaults: clog in PATH) 29 | """ 30 | self.clog = clog 31 | 32 | # Used to specify what command to launch (and to inject faketime) 33 | self._command = [self.clog] 34 | 35 | # Configuration of the isolated environment 36 | self._original_pwd = os.getcwd() 37 | self.datadir = tempfile.mkdtemp(prefix="clog_") 38 | self.clogrc = os.path.join (self.datadir, 'clogrc') 39 | 40 | self._command.extend(['-f', self.clogrc]) 41 | 42 | # Ensure any instance is properly destroyed at session end 43 | atexit.register(lambda: self.destroy()) 44 | 45 | self.reset_env() 46 | 47 | def add_default_extension(self, filename): 48 | """Add default extension to current instance 49 | """ 50 | if not os.path.isdir(self.extdir): 51 | os.mkdir(self.extdir) 52 | 53 | extfile = os.path.join(self.extdir, filename) 54 | if os.path.isfile(extfile): 55 | raise "{} already exists".format(extfile) 56 | 57 | shutil.copy(os.path.join(DEFAULT_EXTENSION_PATH, filename), extfile) 58 | 59 | def __repr__(self): 60 | txt = super(Clog, self).__repr__() 61 | return "{0} running from {1}>".format(txt[:-1], self.datadir) 62 | 63 | def __call__(self, *args, **kwargs): 64 | "aka t = Clog() ; t() which is now an alias to t.runSuccess()" 65 | return self.runSuccess(*args, **kwargs) 66 | 67 | def reset_env(self): 68 | """Set a new environment derived from the one used to launch the test 69 | """ 70 | # Copy all env variables to avoid clashing subprocess environments 71 | self.env = os.environ.copy() 72 | 73 | def config(self, line): 74 | """Add 'line' to self.clogrc. 75 | """ 76 | with open(self.clogrc, "a") as f: 77 | f.write(line + "\n") 78 | 79 | @property 80 | def clogrc_content(self): 81 | """ 82 | Returns the contents of the clogrc file. 83 | """ 84 | with open(self.clogrc, "r") as f: 85 | return f.readlines() 86 | 87 | @staticmethod 88 | def _split_string_args_if_string(args): 89 | """Helper function to parse and split into arguments a single string 90 | argument. The string is literally the same as if written in the shell. 91 | """ 92 | # Enable nicer-looking calls by allowing plain strings 93 | if isinstance(args, STRING_TYPE): 94 | args = shlex.split(args) 95 | 96 | return args 97 | 98 | def runSuccess(self, args="", input=None, merge_streams=False, 99 | timeout=5): 100 | """Invoke clog with given arguments and fail if exit code != 0 101 | 102 | Use runError if you want exit_code to be tested automatically and 103 | *not* fail if program finishes abnormally. 104 | 105 | If you wish to pass instructions to clog such as confirmations or other 106 | input via stdin, you can do so by providing a input string. 107 | Such as input="y\ny\n". 108 | 109 | If merge_streams=True stdout and stderr will be merged into stdout. 110 | 111 | timeout = number of seconds the test will wait for every clog call. 112 | Defaults to 1 second if not specified. Unit is seconds. 113 | 114 | Returns (exit_code, stdout, stderr) if merge_streams=False 115 | (exit_code, output) if merge_streams=True 116 | """ 117 | # Create a copy of the command 118 | command = self._command[:] 119 | 120 | args = self._split_string_args_if_string(args) 121 | command.extend(args) 122 | 123 | output = run_cmd_wait_nofail(command, input, 124 | merge_streams=merge_streams, 125 | env=self.env, 126 | timeout=timeout) 127 | 128 | if output[0] != 0: 129 | raise CommandError(command, *output) 130 | 131 | return output 132 | 133 | def runError(self, args=(), input=None, merge_streams=False, timeout=5): 134 | """Invoke clog with given arguments and fail if exit code == 0 135 | 136 | Use runSuccess if you want exit_code to be tested automatically and 137 | *fail* if program finishes abnormally. 138 | 139 | If you wish to pass instructions to clog such as confirmations or other 140 | input via stdin, you can do so by providing a input string. 141 | Such as input="y\ny\n". 142 | 143 | If merge_streams=True stdout and stderr will be merged into stdout. 144 | 145 | timeout = number of seconds the test will wait for every clog call. 146 | Defaults to 1 second if not specified. Unit is seconds. 147 | 148 | Returns (exit_code, stdout, stderr) if merge_streams=False 149 | (exit_code, output) if merge_streams=True 150 | """ 151 | # Create a copy of the command 152 | command = self._command[:] 153 | 154 | args = self._split_string_args_if_string(args) 155 | command.extend(args) 156 | 157 | output = run_cmd_wait_nofail(command, input, 158 | merge_streams=merge_streams, 159 | env=self.env, 160 | timeout=timeout) 161 | 162 | # output[0] is the exit code 163 | if output[0] == 0 or output[0] is None: 164 | raise CommandError(command, *output) 165 | 166 | return output 167 | 168 | def destroy(self): 169 | """Cleanup the data folder and release server port for other instances 170 | """ 171 | try: 172 | shutil.rmtree(self.datadir) 173 | except OSError as e: 174 | if e.errno == 2: 175 | # Directory no longer exists 176 | pass 177 | else: 178 | raise 179 | 180 | # Prevent future reuse of this instance 181 | self.runSuccess = self.__destroyed 182 | self.runError = self.__destroyed 183 | 184 | # self.destroy will get called when the python session closes. 185 | # If self.destroy was already called, turn the action into a noop 186 | self.destroy = lambda: None 187 | 188 | def __destroyed(self, *args, **kwargs): 189 | raise AttributeError("Program instance has been destroyed. " 190 | "Create a new instance if you need a new client.") 191 | 192 | def faketime(self, faketime=None): 193 | """Set a faketime using libfaketime that will affect the following 194 | command calls. 195 | 196 | If faketime is None, faketime settings will be disabled. 197 | """ 198 | cmd = which("faketime") 199 | if cmd is None: 200 | raise unittest.SkipTest("libfaketime/faketime is not installed") 201 | 202 | if self._command[0] == cmd: 203 | self._command = self._command[3:] 204 | 205 | if faketime is not None: 206 | # Use advanced time format 207 | self._command = [cmd, "-f", faketime] + self._command 208 | 209 | # vim: ai sts=4 et sw=4 210 | -------------------------------------------------------------------------------- /test/basetest/compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | try: 4 | STRING_TYPE = basestring 5 | except NameError: 6 | # Python 3 7 | STRING_TYPE = str 8 | 9 | # vim: ai sts=4 et sw=4 10 | -------------------------------------------------------------------------------- /test/basetest/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import signal 3 | 4 | sig_names = dict((k, v) for v, k in reversed(sorted(signal.__dict__.items())) 5 | if v.startswith('SIG') and not v.startswith('SIG_')) 6 | 7 | 8 | class CommandError(Exception): 9 | def __init__(self, cmd, code, out, err=None, msg=None): 10 | DEFAULT = ("Command '{{0}}' was {signal}'ed. " 11 | "SIGABRT usually means program timed out.\n") 12 | if msg is None: 13 | msg_suffix = "\n*** Start STDOUT ***\n{2}\n*** End STDOUT ***\n" 14 | if err is not None: 15 | msg_suffix += ( 16 | "\n*** Start STDERR ***\n{3}\n*** End STDERR ***\n" 17 | ) 18 | 19 | if code < 0: 20 | self.msg = DEFAULT.format(signal=sig_names[abs(code)]) 21 | else: 22 | self.msg = ("Command '{0}' finished with unexpected exit " 23 | "code '{1}'.\n") 24 | 25 | self.msg += msg_suffix 26 | else: 27 | self.msg = msg 28 | 29 | self.cmd = cmd 30 | self.out = out 31 | self.err = err 32 | self.code = code 33 | 34 | def __str__(self): 35 | return self.msg.format(self.cmd, self.code, self.out, self.err) 36 | 37 | 38 | class HookError(Exception): 39 | pass 40 | 41 | 42 | class TimeoutWaitingFor(object): 43 | def __init__(self, name): 44 | self.name = name 45 | 46 | def __repr__(self): 47 | return "*** Timeout reached while waiting for {0} ***".format( 48 | self.name) 49 | 50 | 51 | class StreamsAreMerged(object): 52 | def __repr__(self): 53 | return "*** Streams are merged, STDERR is not available ***" 54 | 55 | # vim: ai sts=4 et sw=4 56 | -------------------------------------------------------------------------------- /test/basetest/meta.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import print_function, division 4 | 5 | 6 | class MetaTest(type): 7 | """Helper metaclass to simplify dynamic test creation 8 | 9 | Creates test_methods in the TestCase class dynamically named after the 10 | arguments used. 11 | """ 12 | @staticmethod 13 | def make_function(classname, *args, **kwargs): 14 | def test(self): 15 | # ### Body of the usual test_testcase ### # 16 | # Override and redefine this method # 17 | pass 18 | 19 | # Title of test in report 20 | test.__doc__ = "{0}".format(args[0]) 21 | 22 | return test 23 | 24 | def __new__(meta, classname, bases, dct): 25 | tests = dct.get("TESTS") 26 | kwargs = dct.get("EXTRA", {}) 27 | 28 | for i, args in enumerate(tests): 29 | func = meta.make_function(classname, *args, **kwargs) 30 | 31 | # Rename the function after a unique identifier 32 | # Name of function must start with test_ to be ran by unittest 33 | func.__name__ = "test_{0}".format(i) 34 | 35 | # Attach the new test to the testclass 36 | dct[func.__name__] = func 37 | 38 | return super(MetaTest, meta).__new__(meta, classname, bases, dct) 39 | 40 | # vim: ai sts=4 et sw=4 41 | -------------------------------------------------------------------------------- /test/basetest/testing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | import sys 5 | 6 | class BaseTestCase(unittest.TestCase): 7 | def tap(self, out): 8 | sys.stderr.write("--- tap output start ---\n") 9 | for line in out.splitlines(): 10 | sys.stderr.write(line + '\n') 11 | sys.stderr.write("--- tap output end ---\n") 12 | 13 | class TestCase(BaseTestCase): 14 | pass 15 | 16 | # vim: ai sts=4 et sw=4 17 | -------------------------------------------------------------------------------- /test/basetest/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import division 3 | import os 4 | import sys 5 | import socket 6 | import signal 7 | import functools 8 | import atexit 9 | import tempfile 10 | from subprocess import Popen, PIPE, STDOUT 11 | from threading import Thread 12 | try: 13 | from Queue import Queue, Empty 14 | except ImportError: 15 | from queue import Queue, Empty 16 | from time import sleep 17 | try: 18 | import simplejson as json 19 | except ImportError: 20 | import json 21 | from .exceptions import CommandError, TimeoutWaitingFor 22 | 23 | ON_POSIX = 'posix' in sys.builtin_module_names 24 | 25 | # Directory relative to basetest module location 26 | CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) 27 | 28 | # Location of binary files (usually the src/ folder) 29 | BIN_PREFIX = os.path.abspath( 30 | os.path.join(CURRENT_DIR, "..", "..", "src") 31 | ) 32 | 33 | # Default location of test certificates 34 | DEFAULT_CERT_PATH = os.path.abspath( 35 | os.path.join(CURRENT_DIR, "..", "test_certs") 36 | ) 37 | 38 | # Default location of test extensions 39 | DEFAULT_EXTENSION_PATH = os.path.abspath( 40 | os.path.join(CURRENT_DIR, "..", "test_extensions") 41 | ) 42 | 43 | 44 | # Environment flags to control skipping of clog tests 45 | CLOG_SKIP = os.environ.get("CLOG_SKIP", False) 46 | # Environment flags to control use of PATH or in-tree binaries 47 | CLOG_USE_PATH = os.environ.get("CLOG_USE_PATH", False) 48 | 49 | UUID_REGEXP = ("[0-9A-Fa-f]{8}-" + ("[0-9A-Fa-f]{4}-" * 3) + "[0-9A-Fa-f]{12}") 50 | 51 | 52 | def clog_binary_location(cmd="clog"): 53 | """ ../src/ is used by default. 54 | """ 55 | return os.path.join(BIN_PREFIX, cmd) 56 | return binary_location(cmd, CLOG_USE_PATH) 57 | 58 | 59 | def binary_location(cmd, USE_PATH=False): 60 | """ ../src/ is used by default. 61 | """ 62 | return os.path.join(BIN_PREFIX, cmd) 63 | 64 | 65 | def wait_condition(cond, timeout=1, sleeptime=.01): 66 | """Wait for condition to return anything other than None 67 | """ 68 | # NOTE Increasing sleeptime can dramatically increase testsuite runtime 69 | # It also reduces CPU load significantly 70 | if timeout is None: 71 | timeout = 1 72 | 73 | if timeout < sleeptime: 74 | print("Warning, timeout cannot be smaller than", sleeptime) 75 | timeout = sleeptime 76 | 77 | # Max number of attempts until giving up 78 | tries = int(timeout / sleeptime) 79 | 80 | for i in range(tries): 81 | val = cond() 82 | 83 | if val is not None: 84 | break 85 | 86 | sleep(sleeptime) 87 | 88 | return val 89 | 90 | 91 | def wait_process(pid, timeout=None): 92 | """Wait for process to finish 93 | """ 94 | def process(): 95 | try: 96 | os.kill(pid, 0) 97 | except OSError: 98 | # Process is dead 99 | return True 100 | else: 101 | # Process is still ticking 102 | return None 103 | 104 | return wait_condition(process, timeout) 105 | 106 | 107 | def _queue_output(arguments, pidq, outputq): 108 | """Read/Write output/input of given process. 109 | This function is meant to be executed in a thread as it may block 110 | """ 111 | kwargs = arguments["process"] 112 | input = arguments["input"] 113 | 114 | try: 115 | proc = Popen(**kwargs) 116 | except OSError as e: 117 | # pid None is read by the main thread as a crash of the process 118 | pidq.put(None) 119 | 120 | outputq.put(( 121 | "", 122 | ("Unexpected exception caught during execution: '{0}' . ".format(e)), 123 | 255)) # false exitcode 124 | 125 | return 126 | 127 | # Put the PID in the queue for main process to know. 128 | pidq.put(proc.pid) 129 | 130 | # Send input and wait for finish 131 | out, err = proc.communicate(input) 132 | 133 | if sys.version_info > (3,): 134 | out, err = out.decode('utf-8'), err.decode('utf-8') 135 | 136 | # Give the output back to the caller 137 | outputq.put((out, err, proc.returncode)) 138 | 139 | 140 | def _retrieve_output(thread, timeout, queue, thread_error): 141 | """Fetch output from binary subprocess queues 142 | """ 143 | # Try to join the thread on failure abort 144 | thread.join(timeout) 145 | if thread.isAlive(): 146 | # Join should have killed the thread. This is unexpected 147 | raise TimeoutWaitingFor(thread_error + ". Unexpected error") 148 | 149 | # Thread died so we should have output 150 | try: 151 | # data = (stdout, stderr, exitcode) 152 | data = queue.get(timeout=timeout) 153 | except Empty: 154 | data = TimeoutWaitingFor("streams from program") 155 | 156 | return data 157 | 158 | 159 | def _get_output(arguments, timeout=None): 160 | """Collect output from the subprocess without blocking the main process if 161 | subprocess hangs. 162 | """ 163 | # NOTE Increase this value if tests fail with None being received as 164 | # stdout/stderr instead of the expected content 165 | output_timeout = 0.1 # seconds 166 | 167 | pidq = Queue() 168 | outputq = Queue() 169 | 170 | t = Thread(target=_queue_output, args=(arguments, pidq, outputq)) 171 | t.daemon = True 172 | t.start() 173 | 174 | try: 175 | pid = pidq.get(timeout=timeout) 176 | except Empty: 177 | pid = None 178 | 179 | # Process crashed or timed out for some reason 180 | if pid is None: 181 | return _retrieve_output(t, output_timeout, outputq, 182 | "Program to start") 183 | 184 | # Wait for process to finish (normal execution) 185 | state = wait_process(pid, timeout) 186 | 187 | if state: 188 | # Process finished 189 | return _retrieve_output(t, output_timeout, outputq, 190 | "Program thread to join") 191 | 192 | # If we reach this point we assume the process got stuck or timed out 193 | for sig in (signal.SIGABRT, signal.SIGTERM, signal.SIGKILL): 194 | # Start with lower signals and escalate if process ignores them 195 | try: 196 | os.kill(pid, signal.SIGABRT) 197 | except OSError as e: 198 | # 3 means the process finished/died between last check and now 199 | if e.errno != 3: 200 | raise 201 | 202 | # Wait for process to finish (should die/exit after signal) 203 | state = wait_process(pid, timeout) 204 | 205 | if state: 206 | # Process finished 207 | return _retrieve_output(t, output_timeout, outputq, 208 | "Program to die") 209 | 210 | # This should never happen but in case something goes really bad 211 | raise OSError("Program stopped responding and couldn't be killed") 212 | 213 | 214 | def run_cmd_wait(cmd, input=None, stdout=PIPE, stderr=PIPE, 215 | merge_streams=False, env=os.environ, timeout=None): 216 | "Run a subprocess and wait for it to finish" 217 | 218 | if input is None: 219 | stdin = None 220 | else: 221 | stdin = PIPE 222 | 223 | if merge_streams: 224 | stderr = STDOUT 225 | else: 226 | stderr = PIPE 227 | 228 | arguments = { 229 | "process": { 230 | "args": cmd, 231 | "stdin": stdin, 232 | "stdout": stdout, 233 | "stderr": stderr, 234 | "bufsize": 1, 235 | "close_fds": ON_POSIX, 236 | "env": env, 237 | }, 238 | "input": input, 239 | } 240 | out, err, exit = _get_output(arguments, timeout) 241 | 242 | if merge_streams: 243 | if exit != 0: 244 | raise CommandError(cmd, exit, out) 245 | else: 246 | return exit, out 247 | else: 248 | if exit != 0: 249 | raise CommandError(cmd, exit, out, err) 250 | else: 251 | return exit, out, err 252 | 253 | 254 | def run_cmd_wait_nofail(*args, **kwargs): 255 | "Same as run_cmd_wait but silence the exception if it happens" 256 | try: 257 | return run_cmd_wait(*args, **kwargs) 258 | except CommandError as e: 259 | return e.code, e.out, e.err 260 | 261 | 262 | def memoize(obj): 263 | """Keep an in-memory cache of function results given it's inputs 264 | """ 265 | cache = obj.cache = {} 266 | 267 | @functools.wraps(obj) 268 | def memoizer(*args, **kwargs): 269 | key = str(args) + str(kwargs) 270 | if key not in cache: 271 | cache[key] = obj(*args, **kwargs) 272 | return cache[key] 273 | return memoizer 274 | 275 | 276 | try: 277 | from shutil import which 278 | which = memoize(which) 279 | except ImportError: 280 | # NOTE: This is shutil.which backported from python-3.3.3 281 | @memoize 282 | def which(cmd, mode=os.F_OK | os.X_OK, path=None): 283 | """Given a command, mode, and a PATH string, return the path which 284 | conforms to the given mode on the PATH, or None if there is no such 285 | file. 286 | 287 | `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result 288 | of os.environ.get("PATH"), or can be overridden with a custom search 289 | path. 290 | 291 | """ 292 | # Check that a given file can be accessed with the correct mode. 293 | # Additionally check that `file` is not a directory, as on Windows 294 | # directories pass the os.access check. 295 | def _access_check(fn, mode): 296 | return (os.path.exists(fn) and os.access(fn, mode) and 297 | not os.path.isdir(fn)) 298 | 299 | # If we're given a path with a directory part, look it up directly 300 | # rather than referring to PATH directories. This includes checking 301 | # relative to the current directory, e.g. ./script 302 | if os.path.dirname(cmd): 303 | if _access_check(cmd, mode): 304 | return cmd 305 | return None 306 | 307 | if path is None: 308 | path = os.environ.get("PATH", os.defpath) 309 | if not path: 310 | return None 311 | path = path.split(os.pathsep) 312 | 313 | if sys.platform == "win32": 314 | # The current directory takes precedence on Windows. 315 | if os.curdir not in path: 316 | path.insert(0, os.curdir) 317 | 318 | # PATHEXT is necessary to check on Windows. 319 | pathext = os.environ.get("PATHEXT", "").split(os.pathsep) 320 | # See if the given file matches any of the expected path 321 | # extensions. This will allow us to short circuit when given 322 | # "python.exe". If it does match, only test that one, otherwise we 323 | # have to try others. 324 | if any(cmd.lower().endswith(ext.lower()) for ext in pathext): 325 | files = [cmd] 326 | else: 327 | files = [cmd + ext for ext in pathext] 328 | else: 329 | # On other platforms you don't have things like PATHEXT to tell you 330 | # what file suffixes are executable, so just pass on cmd as-is. 331 | files = [cmd] 332 | 333 | seen = set() 334 | for dir in path: 335 | normdir = os.path.normcase(dir) 336 | if normdir not in seen: 337 | seen.add(normdir) 338 | for thefile in files: 339 | name = os.path.join(dir, thefile) 340 | if _access_check(name, mode): 341 | return name 342 | return None 343 | 344 | 345 | def parse_datafile(file): 346 | """Parse .data files, treating files as JSON 347 | """ 348 | data = [] 349 | with open(file) as fh: 350 | for line in fh: 351 | line = line.rstrip("\n") 352 | 353 | # Turn [] strings into {} to be treated properly as JSON hashes 354 | if line.startswith('[') and line.endswith(']'): 355 | line = '{' + line[1:-1] + '}' 356 | 357 | if line.startswith("{"): 358 | data.append(json.loads(line)) 359 | else: 360 | data.append(line) 361 | return data 362 | 363 | 364 | def mkstemp(data): 365 | """ 366 | Create a temporary file that is removed at process exit 367 | """ 368 | def rmtemp(name): 369 | try: 370 | os.remove(name) 371 | except OSError: 372 | pass 373 | 374 | f = tempfile.NamedTemporaryFile(delete=False) 375 | f.write(data) 376 | f.close() 377 | 378 | # Ensure removal at end of python session 379 | atexit.register(rmtemp, f.name) 380 | 381 | return f.name 382 | 383 | 384 | def mkstemp_exec(data): 385 | """Create a temporary executable file that is removed at process exit 386 | """ 387 | name = mkstemp(data) 388 | os.chmod(name, 0o755) 389 | 390 | return name 391 | 392 | # vim: ai sts=4 et sw=4 393 | -------------------------------------------------------------------------------- /test/blank.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | # -*- coding: utf-8 -*- 3 | ############################################################################### 4 | # 5 | # Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez. 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 20 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | # 25 | # http://www.opensource.org/licenses/mit-license.php 26 | # 27 | ############################################################################### 28 | 29 | import sys 30 | import os 31 | import unittest 32 | from datetime import datetime 33 | # Ensure python finds the local simpletap module 34 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 35 | 36 | from basetest import Clog, TestCase 37 | 38 | # Test methods available: 39 | # self.assertEqual(a, b) 40 | # self.assertNotEqual(a, b) 41 | # self.assertTrue(x) 42 | # self.assertFalse(x) 43 | # self.assertIs(a, b) 44 | # self.assertIsNot(substring, text) 45 | # self.assertIsNone(x) 46 | # self.assertIsNotNone(x) 47 | # self.assertIn(substring, text) 48 | # self.assertNotIn(substring, text 49 | # self.assertRaises(e) 50 | # self.assertRegexpMatches(text, pattern) 51 | # self.assertNotRegexpMatches(text, pattern) 52 | # self.tap("") 53 | 54 | class TestBlank(TestCase): 55 | def setUp(self): 56 | """Executed before each test in the class""" 57 | self.t = Clog() 58 | 59 | def test_blank(self): 60 | """Test blank line insertion""" 61 | self.t.config('default rule "bar" --> blank') 62 | 63 | code, out, err = self.t("", input='foo\nfoo\nbar\nbaz\nbaz\n') 64 | self.assertEqual('foo\nfoo\n\nbar\n\nbaz\nbaz\n', out) 65 | 66 | if __name__ == "__main__": 67 | from simpletap import TAPTestRunner 68 | unittest.main(testRunner=TAPTestRunner()) 69 | 70 | -------------------------------------------------------------------------------- /test/decorate.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | # -*- coding: utf-8 -*- 3 | ############################################################################### 4 | # 5 | # Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez. 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 20 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | # 25 | # http://www.opensource.org/licenses/mit-license.php 26 | # 27 | ############################################################################### 28 | 29 | import sys 30 | import os 31 | import unittest 32 | from datetime import datetime 33 | # Ensure python finds the local simpletap module 34 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 35 | 36 | from basetest import Clog, TestCase 37 | 38 | # Test methods available: 39 | # self.assertEqual(a, b) 40 | # self.assertNotEqual(a, b) 41 | # self.assertTrue(x) 42 | # self.assertFalse(x) 43 | # self.assertIs(a, b) 44 | # self.assertIsNot(substring, text) 45 | # self.assertIsNone(x) 46 | # self.assertIsNotNone(x) 47 | # self.assertIn(substring, text) 48 | # self.assertNotIn(substring, text 49 | # self.assertRaises(e) 50 | # self.assertRegexpMatches(text, pattern) 51 | # self.assertNotRegexpMatches(text, pattern) 52 | # self.tap("") 53 | 54 | class TestRegexLine(TestCase): 55 | def setUp(self): 56 | """Executed before each test in the class""" 57 | self.t = Clog() 58 | 59 | def test_decorate_date(self): 60 | """Test decorate a line with the date""" 61 | self.t.config('') 62 | 63 | code, out, err = self.t("--date", input='foo\nbar\n') 64 | self.assertRegexpMatches(out, r'^\d{4}-\d{2}-\d{2} foo\n') 65 | self.assertRegexpMatches(out, r'\n\d{4}-\d{2}-\d{2} bar$') 66 | 67 | def test_decorate_time(self): 68 | """Test decorate a line with time""" 69 | self.t.config('') 70 | 71 | code, out, err = self.t("--time", input='foo\nbar\n') 72 | self.assertRegexpMatches(out, r'^\d{2}:\d{2}:\d{2} foo\n') 73 | self.assertRegexpMatches(out, r'\n\d{2}:\d{2}:\d{2} bar$') 74 | 75 | def test_decorate_date_and_time(self): 76 | """Test decorate a line with date and time""" 77 | self.t.config('') 78 | 79 | code, out, err = self.t("--date --time", input='foo\nbar\n') 80 | self.assertRegexpMatches(out, r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} foo\n') 81 | self.assertRegexpMatches(out, r'\n\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} bar$') 82 | 83 | 84 | if __name__ == "__main__": 85 | from simpletap import TAPTestRunner 86 | unittest.main(testRunner=TAPTestRunner()) 87 | 88 | -------------------------------------------------------------------------------- /test/pattern.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | # -*- coding: utf-8 -*- 3 | ############################################################################### 4 | # 5 | # Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez. 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 20 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | # 25 | # http://www.opensource.org/licenses/mit-license.php 26 | # 27 | ############################################################################### 28 | 29 | import sys 30 | import os 31 | import unittest 32 | from datetime import datetime 33 | # Ensure python finds the local simpletap module 34 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 35 | 36 | from basetest import Clog, TestCase 37 | 38 | # Test methods available: 39 | # self.assertEqual(a, b) 40 | # self.assertNotEqual(a, b) 41 | # self.assertTrue(x) 42 | # self.assertFalse(x) 43 | # self.assertIs(a, b) 44 | # self.assertIsNot(substring, text) 45 | # self.assertIsNone(x) 46 | # self.assertIsNotNone(x) 47 | # self.assertIn(substring, text) 48 | # self.assertNotIn(substring, text 49 | # self.assertRaises(e) 50 | # self.assertRegexpMatches(text, pattern) 51 | # self.assertNotRegexpMatches(text, pattern) 52 | # self.tap("") 53 | 54 | class TestPatternLine(TestCase): 55 | def setUp(self): 56 | """Executed before each test in the class""" 57 | self.t = Clog() 58 | 59 | def test_pattern_line(self): 60 | """Test matching a pattern and coloring a line""" 61 | self.t.config('default rule "foo" --> red line') 62 | 63 | code, out, err = self.t("", input='a foo\na bar\na baz\n') 64 | self.assertIn('\x1b[31ma foo\x1b[0m\n', out) 65 | self.assertRegexpMatches(out, r'\na bar\n') 66 | self.assertRegexpMatches(out, r'\na baz\n') 67 | 68 | class TestPatternMatch(TestCase): 69 | def setUp(self): 70 | """Executed before each test in the class""" 71 | self.t = Clog() 72 | 73 | def test_pattern_match(self): 74 | """Test matching a pattern and coloring a match""" 75 | self.t.config('default rule "foo" --> red match') 76 | 77 | code, out, err = self.t("", input='a foo\na bar\na baz\n') 78 | self.assertIn('a \x1b[31mfoo\x1b[0m\n', out) 79 | self.assertRegexpMatches(out, r'\na bar\n') 80 | self.assertRegexpMatches(out, r'\na baz\n') 81 | 82 | class TestPatternOverlap(TestCase): 83 | def setUp(self): 84 | """Executed before each test in the class""" 85 | self.t = Clog() 86 | 87 | def test_pattern_overlap_1(self): 88 | """Test matching a pattern and coloring a match, red before blue""" 89 | self.t.config('default rule "abc" --> red match') 90 | self.t.config('default rule "cd" --> blue match') 91 | 92 | code, out, err = self.t("", input='abcdabcdabcd\n') 93 | self.tap(out) 94 | self.assertIn('\x1b[31mab\x1b[0m\x1b[34mcd\x1b[0m\x1b[31mab\x1b[0m\x1b[34mcd\x1b[0m\n', out) 95 | 96 | def test_pattern_overlap_2(self): 97 | """Test matching a pattern and coloring a match, blue before red""" 98 | self.t.config('default rule "cd" --> blue match') 99 | self.t.config('default rule "abc" --> red match') 100 | 101 | code, out, err = self.t("", input='abcdabcdabcd\n') 102 | self.tap(out) 103 | self.assertIn('\x1b[31mabc\x1b[0m\x1b[34md\x1b[0m\x1b[31mabc\x1b[0m\x1b[34md\x1b[0m\n', out) 104 | 105 | if __name__ == "__main__": 106 | from simpletap import TAPTestRunner 107 | unittest.main(testRunner=TAPTestRunner()) 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /test/problems: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import print_function 4 | import sys 5 | import re 6 | import argparse 7 | from collections import defaultdict 8 | 9 | 10 | def color(text, c): 11 | """ 12 | Add color on the keyword that identifies the state of the test 13 | """ 14 | if sys.stdout.isatty(): 15 | clear = "\033[0m" 16 | 17 | colors = { 18 | "red": "\033[1m\033[91m", 19 | "yellow": "\033[1m\033[93m", 20 | "green": "\033[1m\033[92m", 21 | } 22 | return colors[c] + text + clear 23 | else: 24 | return text 25 | 26 | 27 | def parse_args(): 28 | parser = argparse.ArgumentParser(description="Report on test results") 29 | parser.add_argument('--summary', action="store_true", 30 | help="Display only the totals in each category") 31 | parser.add_argument('tapfile', default="all.log", nargs="?", 32 | help="File containing TAP output") 33 | return parser.parse_args() 34 | 35 | 36 | def print_category(tests): 37 | if not cmd_args.summary: 38 | for key in sorted(tests): 39 | print("%-32s %4d" % (key, tests[key])) 40 | 41 | 42 | def pad(i): 43 | return " " * i 44 | 45 | 46 | if __name__ == "__main__": 47 | cmd_args = parse_args() 48 | 49 | errors = defaultdict(int) 50 | skipped = defaultdict(int) 51 | expected = defaultdict(int) 52 | unexpected = defaultdict(int) 53 | passed = defaultdict(int) 54 | 55 | file = re.compile("^# (?:./)?(\S+\.t)(?:\.exe)?$") 56 | timestamp = re.compile("^# (\d+(?:\.\d+)?) ==>.*$") 57 | 58 | expected_fail = re.compile(r"^not ok.*?#\s*TODO", re.I) 59 | unexpected_pass = re.compile(r"^ok .*?#\s*TODO", re.I) 60 | skip = re.compile(r"^ok .*?#\s*skip", re.I) 61 | ok = re.compile(r"^ok ", re.I) 62 | not_ok = re.compile(r"^not ok", re.I) 63 | comment = re.compile(r"^#") 64 | plan = re.compile(r"^1..\d+\s*(?:#.*)?$") 65 | 66 | start = None 67 | stop = None 68 | 69 | with open(cmd_args.tapfile) as fh: 70 | for line in fh: 71 | if start is None: 72 | # First line contains the starting timestamp 73 | start = float(timestamp.match(line).group(1)) 74 | continue 75 | 76 | match = file.match(line) 77 | if match: 78 | filename = match.group(1) 79 | 80 | elif expected_fail.match(line): 81 | expected[filename] += 1 82 | 83 | elif unexpected_pass.match(line): 84 | unexpected[filename] += 1 85 | 86 | elif skip.match(line): 87 | skipped[filename] += 1 88 | 89 | # It's important these come last, since they're subpatterns of the above 90 | 91 | elif ok.match(line): 92 | passed[filename] += 1 93 | 94 | elif not_ok.match(line): 95 | errors[filename] += 1 96 | 97 | elif comment.match(line): 98 | pass 99 | 100 | elif plan.match(line): 101 | pass 102 | 103 | else: 104 | # Uncomment if you want to see malformed things we caught as well... 105 | # print(color("Malformed TAP (" + filename + "): " + line, "red")) 106 | pass 107 | 108 | # Last line contains the ending timestamp 109 | stop = float(timestamp.match(line).group(1)) 110 | 111 | v = "{0:>5d}" 112 | passed_str = "Passed:" + pad(24) 113 | passed_int = v.format(sum(passed.values())) 114 | error_str = "Failed:" + pad(24) 115 | error_int = v.format(sum(errors.values())) 116 | unexpected_str = "Unexpected successes:" + pad(10) 117 | unexpected_int = v.format(sum(unexpected.values())) 118 | skipped_str = "Skipped:" + pad(23) 119 | skipped_int = v.format(sum(skipped.values())) 120 | expected_str = "Expected failures:" + pad(13) 121 | expected_int = v.format(sum(expected.values())) 122 | runtime_str = "Runtime:" + pad(20) 123 | runtime_int = "{0:>8.2f} seconds".format(stop - start) 124 | 125 | if cmd_args.summary: 126 | print(color(passed_str, "green"), passed_int) 127 | print(color(error_str, "red"), error_int) 128 | print(color(unexpected_str, "red"), unexpected_int) 129 | print(color(skipped_str, "yellow"), skipped_int) 130 | print(color(expected_str, "yellow"), expected_int) 131 | print(runtime_str, runtime_int) 132 | 133 | else: 134 | print(color(error_str, "red")) 135 | print_category(errors) 136 | print() 137 | print(color(unexpected_str, "red")) 138 | print_category(unexpected) 139 | print() 140 | print(color(skipped_str, "yellow")) 141 | print_category(skipped) 142 | print() 143 | print(color(expected_str, "yellow")) 144 | print_category(expected) 145 | 146 | # If we encoutered any failures, return non-zero code 147 | sys.exit(1 if int(error_int) or int(unexpected_int) else 0) 148 | -------------------------------------------------------------------------------- /test/regex.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | # -*- coding: utf-8 -*- 3 | ############################################################################### 4 | # 5 | # Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez. 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 20 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | # 25 | # http://www.opensource.org/licenses/mit-license.php 26 | # 27 | ############################################################################### 28 | 29 | import sys 30 | import os 31 | import unittest 32 | from datetime import datetime 33 | # Ensure python finds the local simpletap module 34 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 35 | 36 | from basetest import Clog, TestCase 37 | 38 | # Test methods available: 39 | # self.assertEqual(a, b) 40 | # self.assertNotEqual(a, b) 41 | # self.assertTrue(x) 42 | # self.assertFalse(x) 43 | # self.assertIs(a, b) 44 | # self.assertIsNot(substring, text) 45 | # self.assertIsNone(x) 46 | # self.assertIsNotNone(x) 47 | # self.assertIn(substring, text) 48 | # self.assertNotIn(substring, text 49 | # self.assertRaises(e) 50 | # self.assertRegexpMatches(text, pattern) 51 | # self.assertNotRegexpMatches(text, pattern) 52 | # self.tap("") 53 | 54 | class TestRegexLine(TestCase): 55 | def setUp(self): 56 | """Executed before each test in the class""" 57 | self.t = Clog() 58 | 59 | def test_regex_line(self): 60 | """Test matching a regex and coloring a line""" 61 | self.t.config('default rule /foo/ --> red line') 62 | 63 | code, out, err = self.t("", input='a foo\na bar\na baz\n') 64 | self.assertIn('\x1b[31ma foo\x1b[0m\n', out) 65 | self.assertRegexpMatches(out, r'\na bar\n') 66 | self.assertRegexpMatches(out, r'\na baz\n') 67 | 68 | class TestRegexMatch(TestCase): 69 | def setUp(self): 70 | """Executed before each test in the class""" 71 | self.t = Clog() 72 | 73 | def test_regex_match(self): 74 | """Test matching a regex and coloring a match""" 75 | self.t.config('default rule /foo/ --> red match') 76 | 77 | code, out, err = self.t("", input='a foo\na bar\na baz\n') 78 | self.assertIn('a \x1b[31mfoo\x1b[0m\n', out) 79 | self.assertRegexpMatches(out, r'\na bar\n') 80 | self.assertRegexpMatches(out, r'\na baz\n') 81 | 82 | if __name__ == "__main__": 83 | from simpletap import TAPTestRunner 84 | unittest.main(testRunner=TAPTestRunner()) 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /test/rule.t.cpp: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2015 - 2017, Paul Beckingham, Federico Hernandez. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included 13 | // in all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | // 23 | // http://www.opensource.org/licenses/mit-license.php 24 | // 25 | //////////////////////////////////////////////////////////////////////////////// 26 | 27 | #include 28 | #include 29 | #include 30 | 31 | //////////////////////////////////////////////////////////////////////////////// 32 | void testRule ( 33 | UnitTest& t, 34 | const std::string& line, 35 | const std::string& section, 36 | const Color& color, 37 | const std::string& context, 38 | const std::string& fragment) 39 | { 40 | try 41 | { 42 | Rule r (line); 43 | t.ok (r._section == section, "<" + line + "> section match"); 44 | t.ok (r._color == color, "<" + line + "> color match"); 45 | t.ok (r._context == context, "<" + line + "> context match"); 46 | t.ok (r._fragment == fragment, "<" + line + "> fragment match"); 47 | } 48 | catch (...) 49 | { 50 | t.fail ("<" + line + "> section match"); 51 | t.fail ("<" + line + "> color match"); 52 | t.fail ("<" + line + "> context match"); 53 | t.fail ("<" + line + "> fragment match"); 54 | } 55 | } 56 | 57 | //////////////////////////////////////////////////////////////////////////////// 58 | int main (int, char**) 59 | { 60 | UnitTest t (40); 61 | 62 | testRule (t, "default rule /bar/ --> suppress", "default", {}, "suppress", ""); 63 | testRule (t, "default rule /foo/ --> red line", "default", {"red"}, "line", ""); 64 | testRule (t, "default rule /foo/ --> red match", "default", {"red"}, "match", ""); 65 | testRule (t, "default rule \"abc\" --> red match", "default", {"red"}, "match", "abc"); 66 | testRule (t, "default rule \"bar\" --> blank", "default", {}, "blank", "bar"); 67 | testRule (t, "default rule \"cd\" --> blue match", "default", {"blue"}, "match", "cd"); 68 | testRule (t, "default rule \"foo\" --> blank", "default", {}, "blank", "foo"); 69 | testRule (t, "default rule \"foo\" --> red line", "default", {"red"}, "line", "foo"); 70 | testRule (t, "default rule \"foo\" --> red match", "default", {"red"}, "match", "foo"); 71 | testRule (t, "default rule \"foo\" --> suppress", "default", {}, "suppress", "foo"); 72 | 73 | return 0; 74 | } 75 | 76 | //////////////////////////////////////////////////////////////////////////////// 77 | 78 | -------------------------------------------------------------------------------- /test/run_all: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import print_function 5 | import os 6 | import sys 7 | import glob 8 | import argparse 9 | import logging 10 | import time 11 | from multiprocessing import cpu_count 12 | from threading import Thread 13 | from subprocess import call, Popen, PIPE 14 | 15 | if sys.version_info > (3,): 16 | import codecs 17 | 18 | try: 19 | # python 2 20 | from Queue import Queue, Empty 21 | except ImportError: 22 | # python 3 23 | from queue import Queue, Empty 24 | 25 | TIMEOUT = .2 26 | 27 | 28 | def run_test(testqueue, outqueue, threadname): 29 | start = time.time() 30 | while True: 31 | try: 32 | test = testqueue.get(block=True, timeout=TIMEOUT) 33 | except Empty: 34 | break 35 | 36 | log.info("Running test %s", test) 37 | 38 | try: 39 | p = Popen(os.path.abspath(test), stdout=PIPE, stderr=PIPE, 40 | env=os.environ) 41 | out, err = p.communicate() 42 | except Exception as e: 43 | log.exception(e) 44 | # Premature end 45 | break 46 | 47 | if sys.version_info > (3,): 48 | out, err = out.decode('utf-8'), err.decode('utf-8') 49 | 50 | output = ("# {0}\n".format(os.path.basename(test)), out, err) 51 | log.debug("Collected output %s", output) 52 | outqueue.put(output) 53 | 54 | testqueue.task_done() 55 | 56 | log.warning("Finished %s thread after %s seconds", 57 | threadname, round(time.time() - start, 3)) 58 | 59 | 60 | class TestRunner(object): 61 | def __init__(self): 62 | self.threads = [] 63 | if sys.version_info > (3,): 64 | self.tap = open(cmd_args.tapfile, 'w', errors='ignore') 65 | else: 66 | self.tap = open(cmd_args.tapfile, 'w') 67 | 68 | self._parallelq = Queue() 69 | self._serialq = Queue() 70 | self._outputq = Queue() 71 | 72 | def _find_tests(self): 73 | for test in glob.glob("*.t") + glob.glob("*.t.exe"): 74 | if os.access(test, os.X_OK): 75 | # Executables only 76 | if self._is_parallelizable(test): 77 | log.debug("Treating as parallel: %s", test) 78 | self._parallelq.put(test) 79 | else: 80 | log.debug("Treating as serial: %s", test) 81 | self._serialq.put(test) 82 | else: 83 | log.debug("Ignored test %s as it is not executable", test) 84 | 85 | log.info("Parallel tests: %s", self._parallelq.qsize()) 86 | log.info("Serial tests: %s", self._serialq.qsize()) 87 | 88 | def _prepare_threads(self): 89 | # Serial thread 90 | self.threads.append( 91 | Thread(target=run_test, args=(self._serialq, self._outputq, "Serial")) 92 | ) 93 | # Parallel threads 94 | self.threads.extend([ 95 | Thread(target=run_test, args=(self._parallelq, self._outputq, "Parallel")) 96 | for i in range(cpu_count()) 97 | ]) 98 | log.info("Spawned %s threads to run tests", len(self.threads)) 99 | 100 | def _start_threads(self): 101 | for thread in self.threads: 102 | # Threads die when main thread dies 103 | log.debug("Starting thread %s", thread) 104 | thread.daemon = True 105 | thread.start() 106 | 107 | def _print_timestamp_to_tap(self): 108 | now = time.time() 109 | timestamp = "# {0} ==> {1}\n".format( 110 | now, 111 | time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(now)), 112 | ) 113 | 114 | log.debug("Adding timestamp %s to TAP file", timestamp) 115 | self.tap.write(timestamp) 116 | 117 | def _is_parallelizable(self, test): 118 | if cmd_args.serial: 119 | return False 120 | 121 | # This is a pretty weird way to do it, and not realiable. 122 | # We are dealing with some binary tests though. 123 | with open(test, 'rb') as fh: 124 | header = fh.read(100).split(b"\n") 125 | if len(header) >= 2 and \ 126 | ((b"/usr/bin/env python" in header[0]) or \ 127 | (header[1][-14:] == b"bash_tap_tw.sh")): 128 | return True 129 | else: 130 | return False 131 | 132 | def _get_remaining_tests(self): 133 | return self._parallelq.qsize() + self._serialq.qsize() 134 | 135 | def is_running(self): 136 | for thread in self.threads: 137 | if thread.is_alive(): 138 | return True 139 | 140 | return False 141 | 142 | def start(self): 143 | self._find_tests() 144 | self._prepare_threads() 145 | 146 | self._print_timestamp_to_tap() 147 | 148 | finished = 0 149 | total = self._get_remaining_tests() 150 | 151 | self._start_threads() 152 | 153 | while self.is_running() or not self._outputq.empty(): 154 | try: 155 | outputs = self._outputq.get(block=True, timeout=TIMEOUT) 156 | except Empty: 157 | continue 158 | 159 | log.debug("Outputting to TAP: %s", outputs) 160 | 161 | for output in outputs: 162 | self.tap.write(output) 163 | 164 | if cmd_args.verbose: 165 | sys.stdout.write(output) 166 | 167 | self._outputq.task_done() 168 | finished += 1 169 | 170 | log.warning("Finished %s out of %s tests", finished, total) 171 | 172 | self._print_timestamp_to_tap() 173 | 174 | if not self._parallelq.empty() or not self._serialq.empty(): 175 | raise RuntimeError( 176 | "Something went wrong, not all tests were ran. {0} " 177 | "remaining.".format(self._get_remaining_tests())) 178 | 179 | def show_report(self): 180 | self.tap.flush() 181 | sys.stdout.flush() 182 | sys.stderr.flush() 183 | 184 | log.debug("Calling 'problems --summary' for report") 185 | return call([os.path.abspath("problems"), "--summary", cmd_args.tapfile]) 186 | 187 | 188 | def parse_args(): 189 | parser = argparse.ArgumentParser(description="Run Taskwarrior tests") 190 | parser.add_argument('--verbose', '-v', action="store_true", 191 | help="Also send TAP output to stdout") 192 | parser.add_argument('--logging', '-l', action="count", 193 | default=0, 194 | help="Logging level. -lll is the highest level") 195 | parser.add_argument('--serial', action="store_true", 196 | help="Do not run tests in parallel") 197 | parser.add_argument('--tapfile', default="all.log", 198 | help="File to use for TAP output") 199 | return parser.parse_args() 200 | 201 | 202 | def main(): 203 | if sys.version_info > (3,): 204 | sys.stdout = codecs.getwriter("utf-8")(sys.stdout.detach()) 205 | 206 | runner = TestRunner() 207 | runner.start() 208 | 209 | # Propagate the return code 210 | return runner.show_report() 211 | 212 | 213 | if __name__ == "__main__": 214 | cmd_args = parse_args() 215 | 216 | if cmd_args.logging == 1: 217 | level = logging.WARN 218 | elif cmd_args.logging == 2: 219 | level = logging.INFO 220 | elif cmd_args.logging >= 3: 221 | level = logging.DEBUG 222 | else: 223 | level = logging.ERROR 224 | 225 | logging.basicConfig( 226 | format="%(asctime)s - %(levelname)s - %(message)s", 227 | level=level, 228 | ) 229 | log = logging.getLogger(__name__) 230 | 231 | log.debug("Parsed commandline arguments: %s", cmd_args) 232 | 233 | try: 234 | sys.exit(main()) 235 | except Exception as e: 236 | log.exception(e) 237 | sys.exit(1) 238 | 239 | # vim: ai sts=4 et sw=4 240 | -------------------------------------------------------------------------------- /test/simpletap/__init__.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # taskwarrior - a command line task list manager. 3 | # 4 | # Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included 14 | # in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 19 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | # 24 | # http://www.opensource.org/licenses/mit-license.php 25 | # 26 | ############################################################################### 27 | 28 | # Original version by Renato Alves 29 | 30 | import os 31 | import sys 32 | import unittest 33 | import warnings 34 | import traceback 35 | import inspect 36 | 37 | 38 | def color(text, c): 39 | """ 40 | Add color on the keyword that identifies the state of the test 41 | """ 42 | if sys.stdout.isatty(): 43 | clear = "\033[0m" 44 | 45 | colors = { 46 | "red": "\033[1m\033[91m", 47 | "yellow": "\033[1m\033[93m", 48 | "green": "\033[1m\033[92m", 49 | } 50 | return colors[c] + text + clear 51 | else: 52 | return text 53 | 54 | 55 | class TAPTestResult(unittest.result.TestResult): 56 | def __init__(self, stream, descriptions, verbosity): 57 | super(TAPTestResult, self).__init__(stream, descriptions, verbosity) 58 | self.stream = stream 59 | self.descriptions = descriptions 60 | self.verbosity = verbosity 61 | # Buffer stdout and stderr 62 | self.buffer = True 63 | 64 | def getDescription(self, test): 65 | doc_first_line = test.shortDescription() 66 | if self.descriptions and doc_first_line: 67 | return doc_first_line 68 | else: 69 | try: 70 | method = test._testMethodName 71 | except AttributeError: 72 | return "Preparation error on: {0}".format(test.description) 73 | else: 74 | return "{0} ({1})".format(method, test.__class__.__name__) 75 | 76 | def startTestRun(self, total="unk"): 77 | self.stream.writeln("1..{0}".format(total)) 78 | 79 | def stopTest(self, test): 80 | """Prevent flushing of stdout/stderr buffers until later""" 81 | pass 82 | 83 | def _restoreStdout(self): 84 | """Restore sys.stdout and sys.stderr, don't merge buffered output yet 85 | """ 86 | if self.buffer: 87 | sys.stdout = self._original_stdout 88 | sys.stderr = self._original_stderr 89 | 90 | @staticmethod 91 | def _do_stream(data, stream): 92 | """Helper function for _mergeStdout""" 93 | for line in data.splitlines(True): 94 | # newlines should be taken literally and be comments in TAP 95 | line = line.replace("\\n", "\n# ") 96 | 97 | # Add a comment sign before each line 98 | if line.startswith("#"): 99 | stream.write(line) 100 | else: 101 | stream.write("# " + line) 102 | 103 | if not line.endswith('\n'): 104 | stream.write('\n') 105 | 106 | def _mergeStdout(self): 107 | """Merge buffered output with main streams 108 | """ 109 | 110 | if self.buffer: 111 | output = self._stdout_buffer.getvalue() 112 | error = self._stderr_buffer.getvalue() 113 | if output: 114 | self._do_stream(output, sys.stdout) 115 | if error: 116 | self._do_stream(error, sys.stderr) 117 | 118 | self._stdout_buffer.seek(0) 119 | self._stdout_buffer.truncate() 120 | self._stderr_buffer.seek(0) 121 | self._stderr_buffer.truncate() 122 | 123 | # Needed to fix the stopTest override 124 | self._mirrorOutput = False 125 | 126 | def report(self, test, status=None, err=None): 127 | # Restore stdout/stderr but don't flush just yet 128 | self._restoreStdout() 129 | 130 | desc = self.getDescription(test) 131 | 132 | try: 133 | exception, msg, tb = err 134 | except (TypeError, ValueError): 135 | exception_name = "" 136 | msg = err 137 | tb = None 138 | else: 139 | exception_name = exception.__name__ 140 | msg = str(msg) 141 | 142 | trace_msg = "" 143 | 144 | # Extract line where error happened for easier debugging 145 | trace = traceback.extract_tb(tb) 146 | for t in trace: 147 | # t = (filename, line_number, function_name, raw_line) 148 | if t[2].startswith("test"): 149 | trace_msg = " on file {0} line {1} in {2}: '{3}'".format(*t) 150 | break 151 | 152 | # Retrieve the name of the file containing the test 153 | filename = os.path.basename(inspect.getfile(test.__class__)) 154 | 155 | if status: 156 | 157 | if status == "SKIP": 158 | self.stream.writeln("{0} {1} - {2}: {3} # skip".format( 159 | color("ok", "yellow"), self.testsRun, filename, desc) 160 | ) 161 | elif status == "EXPECTED_FAILURE": 162 | self.stream.writeln("{0} {1} - {2}: {3} # TODO".format( 163 | color("not ok", "yellow"), self.testsRun, filename, desc) 164 | ) 165 | else: 166 | self.stream.writeln("{0} {1} - {2}: {3}".format( 167 | color("not ok", "red"), self.testsRun, filename, desc) 168 | ) 169 | 170 | if exception_name: 171 | self.stream.writeln("# {0}: {1}{2}:".format( 172 | status, exception_name, trace_msg) 173 | ) 174 | else: 175 | self.stream.writeln("# {0}:".format(status)) 176 | 177 | # Magic 3 is just for pretty indentation 178 | padding = " " * (len(status) + 3) 179 | 180 | for line in msg.splitlines(): 181 | # Force displaying new-line characters as literal new lines 182 | line = line.replace("\\n", "\n# ") 183 | self.stream.writeln("#{0}{1}".format(padding, line)) 184 | else: 185 | self.stream.writeln("{0} {1} - {2}: {3}".format( 186 | color("ok", "green"), self.testsRun, filename, desc) 187 | ) 188 | 189 | # Flush all buffers to stdout 190 | self._mergeStdout() 191 | 192 | def addSuccess(self, test): 193 | super(TAPTestResult, self).addSuccess(test) 194 | self.report(test) 195 | 196 | def addError(self, test, err): 197 | super(TAPTestResult, self).addError(test, err) 198 | self.report(test, "ERROR", err) 199 | 200 | def addFailure(self, test, err): 201 | super(TAPTestResult, self).addFailure(test, err) 202 | self.report(test, "FAIL", err) 203 | 204 | def addSkip(self, test, reason): 205 | super(TAPTestResult, self).addSkip(test, reason) 206 | self.report(test, "SKIP", reason) 207 | 208 | def addExpectedFailure(self, test, err): 209 | super(TAPTestResult, self).addExpectedFailure(test, err) 210 | self.report(test, "EXPECTED_FAILURE", err) 211 | 212 | def addUnexpectedSuccess(self, test): 213 | super(TAPTestResult, self).addUnexpectedSuccess(test) 214 | self.report(test, "UNEXPECTED_SUCCESS", str(test)) 215 | 216 | 217 | class TAPTestRunner(unittest.runner.TextTestRunner): 218 | """A test runner that displays results using the Test Anything Protocol 219 | syntax. 220 | 221 | Inherits from TextTestRunner the default runner. 222 | """ 223 | resultclass = TAPTestResult 224 | 225 | def run(self, test): 226 | result = self._makeResult() 227 | unittest.signals.registerResult(result) 228 | result.failfast = self.failfast 229 | 230 | # TAP requires output is on STDOUT. 231 | # TODO: Define this at __init__ time 232 | result.stream = unittest.runner._WritelnDecorator(sys.stdout) 233 | 234 | with warnings.catch_warnings(): 235 | if getattr(self, "warnings", None): 236 | # if self.warnings is set, use it to filter all the warnings 237 | warnings.simplefilter(self.warnings) 238 | # if the filter is 'default' or 'always', special-case the 239 | # warnings from the deprecated unittest methods to show them 240 | # no more than once per module, because they can be fairly 241 | # noisy. The -Wd and -Wa flags can be used to bypass this 242 | # only when self.warnings is None. 243 | if self.warnings in ['default', 'always']: 244 | warnings.filterwarnings( 245 | 'module', 246 | category=DeprecationWarning, 247 | message='Please use assert\w+ instead.') 248 | startTestRun = getattr(result, 'startTestRun', None) 249 | if startTestRun is not None: 250 | startTestRun(test.countTestCases()) 251 | try: 252 | test(result) 253 | finally: 254 | stopTestRun = getattr(result, 'stopTestRun', None) 255 | if stopTestRun is not None: 256 | stopTestRun() 257 | 258 | return result 259 | -------------------------------------------------------------------------------- /test/suppress.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | # -*- coding: utf-8 -*- 3 | ############################################################################### 4 | # 5 | # Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez. 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 20 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | # 25 | # http://www.opensource.org/licenses/mit-license.php 26 | # 27 | ############################################################################### 28 | 29 | import sys 30 | import os 31 | import unittest 32 | from datetime import datetime 33 | # Ensure python finds the local simpletap module 34 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 35 | 36 | from basetest import Clog, TestCase 37 | 38 | # Test methods available: 39 | # self.assertEqual(a, b) 40 | # self.assertNotEqual(a, b) 41 | # self.assertTrue(x) 42 | # self.assertFalse(x) 43 | # self.assertIs(a, b) 44 | # self.assertIsNot(substring, text) 45 | # self.assertIsNone(x) 46 | # self.assertIsNotNone(x) 47 | # self.assertIn(substring, text) 48 | # self.assertNotIn(substring, text 49 | # self.assertRaises(e) 50 | # self.assertRegexpMatches(text, pattern) 51 | # self.assertNotRegexpMatches(text, pattern) 52 | # self.tap("") 53 | 54 | class TestSuppress(TestCase): 55 | def setUp(self): 56 | """Executed before each test in the class""" 57 | self.t = Clog() 58 | 59 | def test_suppress_pattern_and_regex(self): 60 | """Test suppress pattern and a regex""" 61 | self.t.config('default rule "foo" --> suppress') 62 | self.t.config('default rule /bar/ --> suppress') 63 | 64 | code, out, err = self.t("", input='a foo\na bar\na baz\n') 65 | self.assertNotIn('a foo', out) 66 | self.assertNotIn('a bar', out) 67 | self.assertIn('a baz', out) 68 | 69 | if __name__ == "__main__": 70 | from simpletap import TAPTestRunner 71 | unittest.main(testRunner=TAPTestRunner()) 72 | 73 | -------------------------------------------------------------------------------- /test/template.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | # -*- coding: utf-8 -*- 3 | ############################################################################### 4 | # 5 | # Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez. 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 20 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | # 25 | # http://www.opensource.org/licenses/mit-license.php 26 | # 27 | ############################################################################### 28 | 29 | import sys 30 | import os 31 | import unittest 32 | from datetime import datetime 33 | # Ensure python finds the local simpletap module 34 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 35 | 36 | from basetest import Timew, TestCase 37 | 38 | # Test methods available: 39 | # self.assertEqual(a, b) 40 | # self.assertNotEqual(a, b) 41 | # self.assertTrue(x) 42 | # self.assertFalse(x) 43 | # self.assertIs(a, b) 44 | # self.assertIsNot(substring, text) 45 | # self.assertIsNone(x) 46 | # self.assertIsNotNone(x) 47 | # self.assertIn(substring, text) 48 | # self.assertNotIn(substring, text 49 | # self.assertRaises(e) 50 | # self.assertRegexpMatches(text, pattern) 51 | # self.assertNotRegexpMatches(text, pattern) 52 | # self.tap("") 53 | 54 | class TestBugNumber(TestCase): 55 | @classmethod 56 | def setUpClass(cls): 57 | """Executed once before any test in the class""" 58 | # Used to initialize objects that can be shared across tests 59 | # Also useful if none of the tests of the current TestCase performs 60 | # data alterations. See tw-285.t for an example 61 | 62 | def setUp(self): 63 | """Executed before each test in the class""" 64 | # Used to initialize objects that should be re-initialized or 65 | # re-created for each individual test 66 | self.t = Clog() 67 | 68 | def test_foo(self): 69 | """Test foo""" 70 | code, out, err = self.t("foo") 71 | self.tap(out) 72 | self.tap(err) 73 | 74 | def test_faketime(self): 75 | """Running tests using libfaketime 76 | 77 | WARNING: 78 | faketime version 0.9.6 and later correctly propagates non-zero 79 | exit codes. Please don't combine faketime tests and 80 | self.t.runError(). 81 | 82 | https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=750721 83 | """ 84 | self.t.faketime("-2y") 85 | 86 | command = ("insert test here") 87 | self.t(command) 88 | 89 | # Remove FAKETIME settings 90 | self.t.faketime() 91 | 92 | code, out, err = self.t("insert test here") 93 | expected = "2.0y" 94 | self.assertIn(expected, out) 95 | 96 | @unittest.skipIf(1 != 0, "This machine has sane logic") 97 | def test_skipped(self): 98 | """Test all logic of the world""" 99 | 100 | @unittest.expectedFailure 101 | def test_expected_failure(self): 102 | """Test something that fails and we know or expect that""" 103 | self.assertEqual(1, 0) 104 | 105 | def tearDown(self): 106 | """Executed after each test in the class""" 107 | 108 | @classmethod 109 | def tearDownClass(cls): 110 | """Executed once after all tests in the class""" 111 | 112 | 113 | if __name__ == "__main__": 114 | from simpletap import TAPTestRunner 115 | unittest.main(testRunner=TAPTestRunner()) 116 | 117 | # vim: ai sts=4 et sw=4 ft=python 118 | -------------------------------------------------------------------------------- /test/test.cpp: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included 13 | // in all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | // 23 | // http://www.opensource.org/licenses/mit-license.php 24 | // 25 | //////////////////////////////////////////////////////////////////////////////// 26 | 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | 36 | /////////////////////////////////////////////////////////////////////////////// 37 | UnitTest::UnitTest () 38 | : _planned (0) 39 | , _counter (0) 40 | , _passed (0) 41 | , _failed (0) 42 | , _skipped (0) 43 | { 44 | } 45 | 46 | /////////////////////////////////////////////////////////////////////////////// 47 | UnitTest::UnitTest (int planned) 48 | : _planned (planned) 49 | , _counter (0) 50 | , _passed (0) 51 | , _failed (0) 52 | , _skipped (0) 53 | { 54 | std::cout << "1.." << _planned << '\n'; 55 | } 56 | 57 | /////////////////////////////////////////////////////////////////////////////// 58 | UnitTest::~UnitTest () 59 | { 60 | float percentPassed = 0.0; 61 | if (_planned > 0) 62 | percentPassed = (100.0 * _passed) / std::max (_planned, _passed + _failed + _skipped); 63 | 64 | if (_counter < _planned) 65 | { 66 | std::cout << "# Only " 67 | << _counter 68 | << " tests, out of a planned " 69 | << _planned 70 | << " were run.\n"; 71 | _skipped += _planned - _counter; 72 | } 73 | 74 | else if (_counter > _planned) 75 | std::cout << "# " 76 | << _counter 77 | << " tests were run, but only " 78 | << _planned 79 | << " were planned.\n"; 80 | 81 | std::cout << "# " 82 | << _passed 83 | << " passed, " 84 | << _failed 85 | << " failed, " 86 | << _skipped 87 | << " skipped. " 88 | << std::setprecision (3) << percentPassed 89 | << "% passed.\n"; 90 | exit (_failed > 0); 91 | } 92 | 93 | /////////////////////////////////////////////////////////////////////////////// 94 | void UnitTest::plan (int planned) 95 | { 96 | _planned = planned; 97 | _counter = 0; 98 | _passed = 0; 99 | _failed = 0; 100 | _skipped = 0; 101 | 102 | std::cout << "1.." << _planned << '\n'; 103 | } 104 | 105 | /////////////////////////////////////////////////////////////////////////////// 106 | void UnitTest::planMore (int extra) 107 | { 108 | _planned += extra; 109 | std::cout << "1.." << _planned << '\n'; 110 | } 111 | 112 | /////////////////////////////////////////////////////////////////////////////// 113 | void UnitTest::ok (bool expression, const std::string& name) 114 | { 115 | ++_counter; 116 | 117 | if (expression) 118 | { 119 | ++_passed; 120 | std::cout << green ("ok") 121 | << " " 122 | << _counter 123 | << " - " 124 | << name 125 | << '\n'; 126 | } 127 | else 128 | { 129 | ++_failed; 130 | std::cout << red ("not ok") 131 | << " " 132 | << _counter 133 | << " - " 134 | << name 135 | << '\n'; 136 | } 137 | } 138 | 139 | /////////////////////////////////////////////////////////////////////////////// 140 | void UnitTest::notok (bool expression, const std::string& name) 141 | { 142 | ++_counter; 143 | 144 | if (!expression) 145 | { 146 | ++_passed; 147 | std::cout << green ("ok") 148 | << " " 149 | << _counter 150 | << " - " 151 | << name 152 | << '\n'; 153 | } 154 | else 155 | { 156 | ++_failed; 157 | std::cout << red ("not ok") 158 | << " " 159 | << _counter 160 | << " - " 161 | << name 162 | << '\n'; 163 | } 164 | } 165 | 166 | /////////////////////////////////////////////////////////////////////////////// 167 | void UnitTest::is (bool actual, bool expected, const std::string& name) 168 | { 169 | ++_counter; 170 | if (actual == expected) 171 | { 172 | ++_passed; 173 | std::cout << green ("ok") 174 | << " " 175 | << _counter 176 | << " - " 177 | << name 178 | << '\n'; 179 | } 180 | else 181 | { 182 | ++_failed; 183 | std::cout << red ("not ok") 184 | << " " 185 | << _counter 186 | << " - " 187 | << name 188 | << "\n# expected: " 189 | << expected 190 | << "\n# got: " 191 | << actual 192 | << '\n'; 193 | } 194 | } 195 | 196 | /////////////////////////////////////////////////////////////////////////////// 197 | void UnitTest::is (size_t actual, size_t expected, const std::string& name) 198 | { 199 | ++_counter; 200 | if (actual == expected) 201 | { 202 | ++_passed; 203 | std::cout << green ("ok") 204 | << " " 205 | << _counter 206 | << " - " 207 | << name 208 | << '\n'; 209 | } 210 | else 211 | { 212 | ++_failed; 213 | std::cout << red ("not ok") 214 | << " " 215 | << _counter 216 | << " - " 217 | << name 218 | << "\n# expected: " 219 | << expected 220 | << "\n# got: " 221 | << actual 222 | << '\n'; 223 | } 224 | } 225 | 226 | /////////////////////////////////////////////////////////////////////////////// 227 | void UnitTest::is (int actual, int expected, const std::string& name) 228 | { 229 | ++_counter; 230 | if (actual == expected) 231 | { 232 | ++_passed; 233 | std::cout << green ("ok") 234 | << " " 235 | << _counter 236 | << " - " 237 | << name 238 | << '\n'; 239 | } 240 | else 241 | { 242 | ++_failed; 243 | std::cout << red ("not ok") 244 | << " " 245 | << _counter 246 | << " - " 247 | << name 248 | << "\n# expected: " 249 | << expected 250 | << "\n# got: " 251 | << actual 252 | << '\n'; 253 | } 254 | } 255 | 256 | /////////////////////////////////////////////////////////////////////////////// 257 | void UnitTest::is (double actual, double expected, const std::string& name) 258 | { 259 | ++_counter; 260 | if (actual == expected) 261 | { 262 | ++_passed; 263 | std::cout << green ("ok") 264 | << " " 265 | << _counter 266 | << " - " 267 | << name 268 | << '\n'; 269 | } 270 | else 271 | { 272 | ++_failed; 273 | std::cout << red ("not ok") 274 | << " " 275 | << _counter 276 | << " - " 277 | << name 278 | << "\n# expected: " 279 | << expected 280 | << "\n# got: " 281 | << actual 282 | << '\n'; 283 | } 284 | } 285 | 286 | /////////////////////////////////////////////////////////////////////////////// 287 | void UnitTest::is (double actual, double expected, double tolerance, const std::string& name) 288 | { 289 | ++_counter; 290 | if (fabs (actual - expected) <= tolerance) 291 | { 292 | ++_passed; 293 | std::cout << green ("ok") 294 | << " " 295 | << _counter 296 | << " - " 297 | << name 298 | << '\n'; 299 | } 300 | else 301 | { 302 | ++_failed; 303 | std::cout << red ("not ok") 304 | << " " 305 | << _counter 306 | << " - " 307 | << name 308 | << "\n# expected: " 309 | << expected 310 | << "\n# got: " 311 | << actual 312 | << '\n'; 313 | } 314 | } 315 | 316 | /////////////////////////////////////////////////////////////////////////////// 317 | void UnitTest::is (unsigned char actual, unsigned char expected, const std::string& name) 318 | { 319 | ++_counter; 320 | if (actual == expected) 321 | { 322 | ++_passed; 323 | std::cout << green ("ok") 324 | << " " 325 | << _counter 326 | << " - " 327 | << name 328 | << '\n'; 329 | } 330 | else 331 | { 332 | ++_failed; 333 | std::cout << red ("not ok") 334 | << " " 335 | << _counter 336 | << " - " 337 | << name 338 | << "\n# expected: " 339 | << expected 340 | << "\n# got: " 341 | << actual 342 | << '\n'; 343 | } 344 | } 345 | 346 | /////////////////////////////////////////////////////////////////////////////// 347 | void UnitTest::is ( 348 | const std::string& actual, 349 | const std::string& expected, 350 | const std::string& name) 351 | { 352 | ++_counter; 353 | if (actual == expected) 354 | { 355 | ++_passed; 356 | std::cout << green ("ok") 357 | << " " 358 | << _counter 359 | << " - " 360 | << name 361 | << '\n'; 362 | } 363 | else 364 | { 365 | ++_failed; 366 | std::cout << red ("not ok") 367 | << " " 368 | << _counter 369 | << " - " 370 | << name 371 | << "\n# expected: '" 372 | << expected 373 | << "'" 374 | << "\n# got: '" 375 | << actual 376 | << "'\n"; 377 | } 378 | } 379 | 380 | /////////////////////////////////////////////////////////////////////////////// 381 | void UnitTest::is ( 382 | const char* actual, 383 | const char* expected, 384 | const std::string& name) 385 | { 386 | ++_counter; 387 | if (! strcmp (actual, expected)) 388 | { 389 | ++_passed; 390 | std::cout << green ("ok") 391 | << " " 392 | << _counter 393 | << " - " 394 | << name 395 | << '\n'; 396 | } 397 | else 398 | { 399 | ++_failed; 400 | std::cout << red ("not ok") 401 | << " " 402 | << _counter 403 | << " - " 404 | << name 405 | << "\n# expected: '" 406 | << expected 407 | << "'" 408 | << "\n# got: '" 409 | << actual 410 | << "'\n"; 411 | } 412 | } 413 | 414 | /////////////////////////////////////////////////////////////////////////////// 415 | void UnitTest::diag (const std::string& text) 416 | { 417 | auto start = text.find_first_not_of (" \t\n\r\f"); 418 | auto end = text.find_last_not_of (" \t\n\r\f"); 419 | if (start != std::string::npos && 420 | end != std::string::npos) 421 | std::cout << "# " << text.substr (start, end - start + 1) << '\n'; 422 | } 423 | 424 | /////////////////////////////////////////////////////////////////////////////// 425 | void UnitTest::pass (const std::string& text) 426 | { 427 | ++_counter; 428 | ++_passed; 429 | std::cout << green ("ok") 430 | << " " 431 | << _counter 432 | << " - " 433 | << text 434 | << '\n'; 435 | } 436 | 437 | /////////////////////////////////////////////////////////////////////////////// 438 | void UnitTest::fail (const std::string& text) 439 | { 440 | ++_counter; 441 | ++_failed; 442 | std::cout << red ("not ok") 443 | << " " 444 | << _counter 445 | << " - " 446 | << text 447 | << '\n'; 448 | } 449 | 450 | /////////////////////////////////////////////////////////////////////////////// 451 | void UnitTest::skip (const std::string& text) 452 | { 453 | ++_counter; 454 | ++_skipped; 455 | std::cout << yellow ("ok") 456 | << " " 457 | << _counter 458 | << " - " 459 | << text 460 | << " # skip" 461 | << '\n'; 462 | } 463 | 464 | /////////////////////////////////////////////////////////////////////////////// 465 | std::string UnitTest::red (const std::string& input) 466 | { 467 | if (isatty (fileno (stdout))) 468 | return std::string ("\033[31m" + input + "\033[0m"); 469 | 470 | return input; 471 | } 472 | 473 | /////////////////////////////////////////////////////////////////////////////// 474 | std::string UnitTest::green (const std::string& input) 475 | { 476 | if (isatty (fileno (stdout))) 477 | return std::string ("\033[32m" + input + "\033[0m"); 478 | 479 | return input; 480 | } 481 | 482 | /////////////////////////////////////////////////////////////////////////////// 483 | std::string UnitTest::yellow (const std::string& input) 484 | { 485 | if (isatty (fileno (stdout))) 486 | return std::string ("\033[33m" + input + "\033[0m"); 487 | 488 | return input; 489 | } 490 | 491 | /////////////////////////////////////////////////////////////////////////////// 492 | -------------------------------------------------------------------------------- /test/test.h: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included 13 | // in all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | // 23 | // http://www.opensource.org/licenses/mit-license.php 24 | // 25 | //////////////////////////////////////////////////////////////////////////////// 26 | 27 | #ifndef INCLUDED_UNITTEST 28 | #define INCLUDED_UNITTEST 29 | 30 | #include 31 | 32 | class UnitTest 33 | { 34 | public: 35 | UnitTest (); 36 | UnitTest (int); 37 | ~UnitTest (); 38 | 39 | void plan (int); 40 | void planMore (int); 41 | void ok (bool, const std::string&); 42 | void notok (bool, const std::string&); 43 | void is (bool, bool, const std::string&); 44 | void is (size_t, size_t, const std::string&); 45 | void is (int, int, const std::string&); 46 | void is (double, double, const std::string&); 47 | void is (double, double, double, const std::string&); 48 | void is (unsigned char, unsigned char, const std::string&); 49 | void is (const std::string&, const std::string&, const std::string&); 50 | void is (const char*, const char*, const std::string&); 51 | void diag (const std::string&); 52 | void pass (const std::string&); 53 | void fail (const std::string&); 54 | void skip (const std::string&); 55 | 56 | private: 57 | std::string red (const std::string&); 58 | std::string green (const std::string&); 59 | std::string yellow (const std::string&); 60 | 61 | private: 62 | int _planned; 63 | int _counter; 64 | int _passed; 65 | int _failed; 66 | int _skipped; 67 | }; 68 | 69 | #endif 70 | 71 | //////////////////////////////////////////////////////////////////////////////// 72 | -------------------------------------------------------------------------------- /test/version.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | # -*- coding: utf-8 -*- 3 | ############################################################################### 4 | # 5 | # Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez. 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 20 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | # 25 | # http://www.opensource.org/licenses/mit-license.php 26 | # 27 | ############################################################################### 28 | 29 | import sys 30 | import os 31 | import unittest 32 | import re 33 | # Ensure python finds the local simpletap module 34 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 35 | 36 | from basetest import Clog, TestCase 37 | from basetest.utils import run_cmd_wait 38 | 39 | # Test methods available: 40 | # self.assertEqual(a, b) 41 | # self.assertNotEqual(a, b) 42 | # self.assertTrue(x) 43 | # self.assertFalse(x) 44 | # self.assertIs(a, b) 45 | # self.assertIsNot(substring, text) 46 | # self.assertIsNone(x) 47 | # self.assertIsNotNone(x) 48 | # self.assertIn(substring, text) 49 | # self.assertNotIn(substring, text 50 | # self.assertRaises(e) 51 | # self.assertRegexpMatches(text, pattern) 52 | # self.assertNotRegexpMatches(text, pattern) 53 | # self.tap("") 54 | 55 | class TestVersion(TestCase): 56 | def setUp(self): 57 | self.t = Clog() 58 | 59 | def test_version_option(self): 60 | """Verify that 'clog --version' returns something valid""" 61 | code, out, err = self.t("--version") 62 | self.assertRegexpMatches(out, r'clog \d+\.\d+\.\d+ built for') 63 | 64 | 65 | if __name__ == "__main__": 66 | from simpletap import TAPTestRunner 67 | unittest.main(testRunner=TAPTestRunner()) 68 | 69 | # vim: ai sts=4 et sw=4 ft=python 70 | --------------------------------------------------------------------------------