Commit 6b58e3302cdee48a75b74b065cbb9138f51dc167
Committed by
GitHub
Merge pull request #796 from christian-intra2net/unittest-automation
Run unittests automatically on github
Showing
28 changed files
with
414 additions
and
173 deletions
.github/workflows/unittests.yml
0 → 100644
| 1 | +name: Python package | ||
| 2 | + | ||
| 3 | +on: | ||
| 4 | + push: | ||
| 5 | + branches: [master] | ||
| 6 | + pull_request: | ||
| 7 | + branches: [master] | ||
| 8 | + | ||
| 9 | +jobs: | ||
| 10 | + check: | ||
| 11 | + runs-on: ${{ matrix.os }} | ||
| 12 | + strategy: | ||
| 13 | + matrix: | ||
| 14 | + os: ["ubuntu-latest", "windows-latest", "macos-latest"] | ||
| 15 | + python-version: ["3.x", "pypy-3.9"] | ||
| 16 | + include: | ||
| 17 | + - python-version: 3.x | ||
| 18 | + runlint: 1 | ||
| 19 | + | ||
| 20 | + steps: | ||
| 21 | + - uses: actions/checkout@v3 | ||
| 22 | + - name: Set up Python ${{ matrix.python-version }} | ||
| 23 | + uses: actions/setup-python@v3 | ||
| 24 | + with: | ||
| 25 | + python-version: ${{ matrix.python-version }} | ||
| 26 | + - name: Install dependencies | ||
| 27 | + run: | | ||
| 28 | + python -c "import sys; import platform; print(sys.version); print(sys.platform); print(platform.python_implementation()); print(platform.system())" | ||
| 29 | + cat requirements.txt | ||
| 30 | + python -m pip install --upgrade pip | ||
| 31 | + pip install -r requirements.txt | ||
| 32 | + pip install pylint | ||
| 33 | + - name: Run pylint | ||
| 34 | + if: ${{ matrix.runlint }} | ||
| 35 | + run: pylint -E --ignore=thirdparty oletools tests | ||
| 36 | + - name: Run unittests | ||
| 37 | + run: python -m unittest discover -f | ||
| 0 | \ No newline at end of file | 38 | \ No newline at end of file |
oletools/common/io_encoding.py
| @@ -58,7 +58,7 @@ PY3 = sys.version_info.major >= 3 | @@ -58,7 +58,7 @@ PY3 = sys.version_info.major >= 3 | ||
| 58 | if PY3: | 58 | if PY3: |
| 59 | from builtins import open as builtin_open | 59 | from builtins import open as builtin_open |
| 60 | else: | 60 | else: |
| 61 | - from __builtin__ import open as builtin_open | 61 | + from __builtin__ import open as builtin_open # pylint: disable=import-error |
| 62 | 62 | ||
| 63 | # -- CONSTANTS ---------------------------------------------------------------- | 63 | # -- CONSTANTS ---------------------------------------------------------------- |
| 64 | #: encoding to use for redirection if no good encoding can be found | 64 | #: encoding to use for redirection if no good encoding can be found |
oletools/common/log_helper/_json_formatter.py
| @@ -5,12 +5,17 @@ import json | @@ -5,12 +5,17 @@ import json | ||
| 5 | class JsonFormatter(logging.Formatter): | 5 | class JsonFormatter(logging.Formatter): |
| 6 | """ | 6 | """ |
| 7 | Format every message to be logged as a JSON object | 7 | Format every message to be logged as a JSON object |
| 8 | + | ||
| 9 | + Uses the standard :py:class:`logging.Formatter` with standard arguments | ||
| 10 | + to do the actual formatting, could save and use a user-supplied formatter | ||
| 11 | + instead. | ||
| 8 | """ | 12 | """ |
| 9 | _is_first_line = True | 13 | _is_first_line = True |
| 10 | 14 | ||
| 11 | def __init__(self, other_logger_has_first_line=False): | 15 | def __init__(self, other_logger_has_first_line=False): |
| 12 | if other_logger_has_first_line: | 16 | if other_logger_has_first_line: |
| 13 | self._is_first_line = False | 17 | self._is_first_line = False |
| 18 | + self.msg_formatter = logging.Formatter() # could adjust this | ||
| 14 | 19 | ||
| 15 | def format(self, record): | 20 | def format(self, record): |
| 16 | """ | 21 | """ |
| @@ -18,12 +23,34 @@ class JsonFormatter(logging.Formatter): | @@ -18,12 +23,34 @@ class JsonFormatter(logging.Formatter): | ||
| 18 | the output JSON-compatible. The only exception is when printing the first line, | 23 | the output JSON-compatible. The only exception is when printing the first line, |
| 19 | so we need to keep track of it. | 24 | so we need to keep track of it. |
| 20 | 25 | ||
| 21 | - We assume that all input comes from the OletoolsLoggerAdapter which | ||
| 22 | - ensures that there is a `type` field in the record. Otherwise will have | ||
| 23 | - to add a try-except around the access to `record.type`. | 26 | + The actual conversion from :py:class:`logging.LogRecord` to a text message |
| 27 | + (i.e. %-formatting, adding exception information, etc.) is delegated to the | ||
| 28 | + standard :py:class:`logging.Formatter. | ||
| 29 | + | ||
| 30 | + The dumped json structure contains fields `msg` with the formatted message, | ||
| 31 | + `level` with the log-level of the message and `type`, which is created by | ||
| 32 | + :py:class:`oletools.common.log_helper.OletoolsLoggerAdapter` or added here | ||
| 33 | + (for input from e.g. captured warnings, third-party libraries) | ||
| 24 | """ | 34 | """ |
| 25 | - json_dict = dict(msg=record.msg.replace('\n', ' '), level=record.levelname) | ||
| 26 | - json_dict['type'] = record.type | 35 | + json_dict = dict(msg='', level='', type='') |
| 36 | + try: | ||
| 37 | + msg = self.msg_formatter.format(record) | ||
| 38 | + json_dict['msg'] = msg.replace('\n', ' ') | ||
| 39 | + json_dict['level'] = record.levelname | ||
| 40 | + json_dict['type'] = record.type | ||
| 41 | + except AttributeError: # most probably: record has no "type" field | ||
| 42 | + if record.name == 'py.warnings': # this is from python's warning-capture logger | ||
| 43 | + json_dict['type'] = 'warning' | ||
| 44 | + else: | ||
| 45 | + json_dict['type'] = 'msg' # message of unknown origin | ||
| 46 | + except Exception as exc: | ||
| 47 | + try: | ||
| 48 | + json_dict['msg'] = "Ignore {0} when formatting '{1}': {2}".format(type(exc), record.msg, exc) | ||
| 49 | + except Exception as exc2: | ||
| 50 | + json_dict['msg'] = 'Caught {0} in logging'.format(str(exc2)) | ||
| 51 | + json_dict['type'] = 'log-warning' | ||
| 52 | + json_dict['level'] = 'warning' | ||
| 53 | + | ||
| 27 | formatted_message = ' ' + json.dumps(json_dict) | 54 | formatted_message = ' ' + json.dumps(json_dict) |
| 28 | 55 | ||
| 29 | if self._is_first_line: | 56 | if self._is_first_line: |
oletools/common/log_helper/_logger_adapter.py
| @@ -7,6 +7,7 @@ class OletoolsLoggerAdapter(logging.LoggerAdapter): | @@ -7,6 +7,7 @@ class OletoolsLoggerAdapter(logging.LoggerAdapter): | ||
| 7 | Adapter class for all loggers returned by the logging module. | 7 | Adapter class for all loggers returned by the logging module. |
| 8 | """ | 8 | """ |
| 9 | _json_enabled = None | 9 | _json_enabled = None |
| 10 | + _is_warn_logger = False # this is always False | ||
| 10 | 11 | ||
| 11 | def print_str(self, message, **kwargs): | 12 | def print_str(self, message, **kwargs): |
| 12 | """ | 13 | """ |
| @@ -44,7 +45,10 @@ class OletoolsLoggerAdapter(logging.LoggerAdapter): | @@ -44,7 +45,10 @@ class OletoolsLoggerAdapter(logging.LoggerAdapter): | ||
| 44 | kwargs['extra']['type'] = kwargs['type'] | 45 | kwargs['extra']['type'] = kwargs['type'] |
| 45 | del kwargs['type'] # downstream loggers cannot deal with this | 46 | del kwargs['type'] # downstream loggers cannot deal with this |
| 46 | if 'type' not in kwargs['extra']: | 47 | if 'type' not in kwargs['extra']: |
| 47 | - kwargs['extra']['type'] = 'msg' # type will be added to LogRecord | 48 | + if self._is_warn_logger: |
| 49 | + kwargs['extra']['type'] = 'warning' # this will add field | ||
| 50 | + else: | ||
| 51 | + kwargs['extra']['type'] = 'msg' # 'type' to LogRecord | ||
| 48 | return msg, kwargs | 52 | return msg, kwargs |
| 49 | 53 | ||
| 50 | def set_json_enabled_function(self, json_enabled): | 54 | def set_json_enabled_function(self, json_enabled): |
| @@ -53,6 +57,12 @@ class OletoolsLoggerAdapter(logging.LoggerAdapter): | @@ -53,6 +57,12 @@ class OletoolsLoggerAdapter(logging.LoggerAdapter): | ||
| 53 | """ | 57 | """ |
| 54 | self._json_enabled = json_enabled | 58 | self._json_enabled = json_enabled |
| 55 | 59 | ||
| 60 | + def set_warnings_logger(self): | ||
| 61 | + """Make this the logger for warnings""" | ||
| 62 | + # create a object attribute that shadows the class attribute which is | ||
| 63 | + # always False | ||
| 64 | + self._is_warn_logger = True | ||
| 65 | + | ||
| 56 | def level(self): | 66 | def level(self): |
| 57 | """Return current level of logger.""" | 67 | """Return current level of logger.""" |
| 58 | return self.logger.level | 68 | return self.logger.level |
oletools/common/log_helper/log_helper.py
| @@ -152,6 +152,11 @@ class LogHelper: | @@ -152,6 +152,11 @@ class LogHelper: | ||
| 152 | self._use_json = use_json | 152 | self._use_json = use_json |
| 153 | sys.excepthook = self._get_except_hook(sys.excepthook) | 153 | sys.excepthook = self._get_except_hook(sys.excepthook) |
| 154 | 154 | ||
| 155 | + # make sure warnings do not mess up our output | ||
| 156 | + logging.captureWarnings(True) | ||
| 157 | + warn_logger = self.get_or_create_silent_logger('py.warnings') | ||
| 158 | + warn_logger.set_warnings_logger() | ||
| 159 | + | ||
| 155 | # since there could be loggers already created we go through all of them | 160 | # since there could be loggers already created we go through all of them |
| 156 | # and set their levels to 0 so they will use the root logger's level | 161 | # and set their levels to 0 so they will use the root logger's level |
| 157 | for name in self._all_names: | 162 | for name in self._all_names: |
| @@ -174,6 +179,7 @@ class LogHelper: | @@ -174,6 +179,7 @@ class LogHelper: | ||
| 174 | 179 | ||
| 175 | # end logging | 180 | # end logging |
| 176 | self._all_names = set() | 181 | self._all_names = set() |
| 182 | + logging.captureWarnings(False) | ||
| 177 | logging.shutdown() | 183 | logging.shutdown() |
| 178 | 184 | ||
| 179 | # end json list | 185 | # end json list |
oletools/doc/Contribute.md
| @@ -13,6 +13,8 @@ to **send feedback**. | @@ -13,6 +13,8 @@ to **send feedback**. | ||
| 13 | 13 | ||
| 14 | The code is available in [a repository on GitHub](https://github.com/decalage2/oletools). | 14 | The code is available in [a repository on GitHub](https://github.com/decalage2/oletools). |
| 15 | You may use it to **submit enhancements** using forks and pull requests. | 15 | You may use it to **submit enhancements** using forks and pull requests. |
| 16 | +When submitting a PR, GitHub will automatically check that unittests pass and | ||
| 17 | +`pylint -E` does not report anything for the code files you changed. | ||
| 16 | 18 | ||
| 17 | -------------------------------------------------------------------------- | 19 | -------------------------------------------------------------------------- |
| 18 | 20 |
oletools/mraptor_milter.py
| @@ -69,7 +69,7 @@ __version__ = '0.51' | @@ -69,7 +69,7 @@ __version__ = '0.51' | ||
| 69 | 69 | ||
| 70 | # --- IMPORTS ---------------------------------------------------------------- | 70 | # --- IMPORTS ---------------------------------------------------------------- |
| 71 | 71 | ||
| 72 | -import Milter | 72 | +import Milter # not part of requirements, therefore: # pylint: disable=import-error |
| 73 | import io | 73 | import io |
| 74 | import time | 74 | import time |
| 75 | import email | 75 | import email |
| @@ -78,7 +78,7 @@ import os | @@ -78,7 +78,7 @@ import os | ||
| 78 | import logging | 78 | import logging |
| 79 | import logging.handlers | 79 | import logging.handlers |
| 80 | import datetime | 80 | import datetime |
| 81 | -import StringIO | 81 | +import StringIO # not part of requirements, therefore: # pylint: disable=import-error |
| 82 | 82 | ||
| 83 | from socket import AF_INET6 | 83 | from socket import AF_INET6 |
| 84 | 84 | ||
| @@ -96,7 +96,7 @@ if not _parent_dir in sys.path: | @@ -96,7 +96,7 @@ if not _parent_dir in sys.path: | ||
| 96 | 96 | ||
| 97 | from oletools import olevba, mraptor | 97 | from oletools import olevba, mraptor |
| 98 | 98 | ||
| 99 | -from Milter.utils import parse_addr | 99 | +from Milter.utils import parse_addr # not part of requirements, therefore: # pylint: disable=import-error |
| 100 | 100 | ||
| 101 | from zipfile import is_zipfile | 101 | from zipfile import is_zipfile |
| 102 | 102 | ||
| @@ -389,7 +389,7 @@ if __name__ == "__main__": | @@ -389,7 +389,7 @@ if __name__ == "__main__": | ||
| 389 | 389 | ||
| 390 | # Using daemonize: | 390 | # Using daemonize: |
| 391 | # See http://daemonize.readthedocs.io/en/latest/ | 391 | # See http://daemonize.readthedocs.io/en/latest/ |
| 392 | - from daemonize import Daemonize | 392 | + from daemonize import Daemonize # not part of requirements, therefore: # pylint: disable=import-error |
| 393 | daemon = Daemonize(app="mraptor_milter", pid=PIDFILE, action=main) | 393 | daemon = Daemonize(app="mraptor_milter", pid=PIDFILE, action=main) |
| 394 | daemon.start() | 394 | daemon.start() |
| 395 | 395 |
oletools/msodde.py
| @@ -149,69 +149,69 @@ LOCATIONS = ('word/document.xml', 'word/endnotes.xml', 'word/footnotes.xml', | @@ -149,69 +149,69 @@ LOCATIONS = ('word/document.xml', 'word/endnotes.xml', 'word/footnotes.xml', | ||
| 149 | # switches_with_args, switches_without_args, format_switches) | 149 | # switches_with_args, switches_without_args, format_switches) |
| 150 | FIELD_BLACKLIST = ( | 150 | FIELD_BLACKLIST = ( |
| 151 | # date and time: | 151 | # date and time: |
| 152 | - ('CREATEDATE', 0, 0, '', 'hs', 'datetime'), # pylint: disable=bad-whitespace | ||
| 153 | - ('DATE', 0, 0, '', 'hls', 'datetime'), # pylint: disable=bad-whitespace | ||
| 154 | - ('EDITTIME', 0, 0, '', '', 'numeric'), # pylint: disable=bad-whitespace | ||
| 155 | - ('PRINTDATE', 0, 0, '', 'hs', 'datetime'), # pylint: disable=bad-whitespace | ||
| 156 | - ('SAVEDATE', 0, 0, '', 'hs', 'datetime'), # pylint: disable=bad-whitespace | ||
| 157 | - ('TIME', 0, 0, '', '', 'datetime'), # pylint: disable=bad-whitespace | 152 | + ('CREATEDATE', 0, 0, '', 'hs', 'datetime'), |
| 153 | + ('DATE', 0, 0, '', 'hls', 'datetime'), | ||
| 154 | + ('EDITTIME', 0, 0, '', '', 'numeric'), | ||
| 155 | + ('PRINTDATE', 0, 0, '', 'hs', 'datetime'), | ||
| 156 | + ('SAVEDATE', 0, 0, '', 'hs', 'datetime'), | ||
| 157 | + ('TIME', 0, 0, '', '', 'datetime'), | ||
| 158 | # exclude document automation (we hate the "auto" in "automation") | 158 | # exclude document automation (we hate the "auto" in "automation") |
| 159 | # (COMPARE, DOCVARIABLE, GOTOBUTTON, IF, MACROBUTTON, PRINT) | 159 | # (COMPARE, DOCVARIABLE, GOTOBUTTON, IF, MACROBUTTON, PRINT) |
| 160 | # document information | 160 | # document information |
| 161 | - ('AUTHOR', 0, 1, '', '', 'string'), # pylint: disable=bad-whitespace | ||
| 162 | - ('COMMENTS', 0, 1, '', '', 'string'), # pylint: disable=bad-whitespace | ||
| 163 | - ('DOCPROPERTY', 1, 0, '', '', 'string/numeric/datetime'), # pylint: disable=bad-whitespace | ||
| 164 | - ('FILENAME', 0, 0, '', 'p', 'string'), # pylint: disable=bad-whitespace | ||
| 165 | - ('FILESIZE', 0, 0, '', 'km', 'numeric'), # pylint: disable=bad-whitespace | ||
| 166 | - ('KEYWORDS', 0, 1, '', '', 'string'), # pylint: disable=bad-whitespace | ||
| 167 | - ('LASTSAVEDBY', 0, 0, '', '', 'string'), # pylint: disable=bad-whitespace | ||
| 168 | - ('NUMCHARS', 0, 0, '', '', 'numeric'), # pylint: disable=bad-whitespace | ||
| 169 | - ('NUMPAGES', 0, 0, '', '', 'numeric'), # pylint: disable=bad-whitespace | ||
| 170 | - ('NUMWORDS', 0, 0, '', '', 'numeric'), # pylint: disable=bad-whitespace | ||
| 171 | - ('SUBJECT', 0, 1, '', '', 'string'), # pylint: disable=bad-whitespace | ||
| 172 | - ('TEMPLATE', 0, 0, '', 'p', 'string'), # pylint: disable=bad-whitespace | ||
| 173 | - ('TITLE', 0, 1, '', '', 'string'), # pylint: disable=bad-whitespace | 161 | + ('AUTHOR', 0, 1, '', '', 'string'), |
| 162 | + ('COMMENTS', 0, 1, '', '', 'string'), | ||
| 163 | + ('DOCPROPERTY', 1, 0, '', '', 'string/numeric/datetime'), | ||
| 164 | + ('FILENAME', 0, 0, '', 'p', 'string'), | ||
| 165 | + ('FILESIZE', 0, 0, '', 'km', 'numeric'), | ||
| 166 | + ('KEYWORDS', 0, 1, '', '', 'string'), | ||
| 167 | + ('LASTSAVEDBY', 0, 0, '', '', 'string'), | ||
| 168 | + ('NUMCHARS', 0, 0, '', '', 'numeric'), | ||
| 169 | + ('NUMPAGES', 0, 0, '', '', 'numeric'), | ||
| 170 | + ('NUMWORDS', 0, 0, '', '', 'numeric'), | ||
| 171 | + ('SUBJECT', 0, 1, '', '', 'string'), | ||
| 172 | + ('TEMPLATE', 0, 0, '', 'p', 'string'), | ||
| 173 | + ('TITLE', 0, 1, '', '', 'string'), | ||
| 174 | # equations and formulas | 174 | # equations and formulas |
| 175 | # exlude '=' formulae because they have different syntax (and can be bad) | 175 | # exlude '=' formulae because they have different syntax (and can be bad) |
| 176 | - ('ADVANCE', 0, 0, 'dlruxy', '', ''), # pylint: disable=bad-whitespace | ||
| 177 | - ('SYMBOL', 1, 0, 'fs', 'ahju', ''), # pylint: disable=bad-whitespace | 176 | + ('ADVANCE', 0, 0, 'dlruxy', '', ''), |
| 177 | + ('SYMBOL', 1, 0, 'fs', 'ahju', ''), | ||
| 178 | # form fields | 178 | # form fields |
| 179 | - ('FORMCHECKBOX', 0, 0, '', '', ''), # pylint: disable=bad-whitespace | ||
| 180 | - ('FORMDROPDOWN', 0, 0, '', '', ''), # pylint: disable=bad-whitespace | ||
| 181 | - ('FORMTEXT', 0, 0, '', '', ''), # pylint: disable=bad-whitespace | 179 | + ('FORMCHECKBOX', 0, 0, '', '', ''), |
| 180 | + ('FORMDROPDOWN', 0, 0, '', '', ''), | ||
| 181 | + ('FORMTEXT', 0, 0, '', '', ''), | ||
| 182 | # index and tables | 182 | # index and tables |
| 183 | - ('INDEX', 0, 0, 'bcdefghklpsz', 'ry', ''), # pylint: disable=bad-whitespace | 183 | + ('INDEX', 0, 0, 'bcdefghklpsz', 'ry', ''), |
| 184 | # exlude RD since that imports data from other files | 184 | # exlude RD since that imports data from other files |
| 185 | - ('TA', 0, 0, 'clrs', 'bi', ''), # pylint: disable=bad-whitespace | ||
| 186 | - ('TC', 1, 0, 'fl', 'n', ''), # pylint: disable=bad-whitespace | ||
| 187 | - ('TOA', 0, 0, 'bcdegls', 'fhp', ''), # pylint: disable=bad-whitespace | ||
| 188 | - ('TOC', 0, 0, 'abcdflnopst', 'huwxz', ''), # pylint: disable=bad-whitespace | ||
| 189 | - ('XE', 1, 0, 'frty', 'bi', ''), # pylint: disable=bad-whitespace | 185 | + ('TA', 0, 0, 'clrs', 'bi', ''), |
| 186 | + ('TC', 1, 0, 'fl', 'n', ''), | ||
| 187 | + ('TOA', 0, 0, 'bcdegls', 'fhp', ''), | ||
| 188 | + ('TOC', 0, 0, 'abcdflnopst', 'huwxz', ''), | ||
| 189 | + ('XE', 1, 0, 'frty', 'bi', ''), | ||
| 190 | # links and references | 190 | # links and references |
| 191 | # exclude AUTOTEXT and AUTOTEXTLIST since we do not like stuff with 'AUTO' | 191 | # exclude AUTOTEXT and AUTOTEXTLIST since we do not like stuff with 'AUTO' |
| 192 | - ('BIBLIOGRAPHY', 0, 0, 'lfm', '', ''), # pylint: disable=bad-whitespace | ||
| 193 | - ('CITATION', 1, 0, 'lfspvm', 'nty', ''), # pylint: disable=bad-whitespace | 192 | + ('BIBLIOGRAPHY', 0, 0, 'lfm', '', ''), |
| 193 | + ('CITATION', 1, 0, 'lfspvm', 'nty', ''), | ||
| 194 | # exclude HYPERLINK since we are allergic to URLs | 194 | # exclude HYPERLINK since we are allergic to URLs |
| 195 | # exclude INCLUDEPICTURE and INCLUDETEXT (other file or maybe even URL?) | 195 | # exclude INCLUDEPICTURE and INCLUDETEXT (other file or maybe even URL?) |
| 196 | # exclude LINK and REF (could reference other files) | 196 | # exclude LINK and REF (could reference other files) |
| 197 | - ('NOTEREF', 1, 0, '', 'fhp', ''), # pylint: disable=bad-whitespace | ||
| 198 | - ('PAGEREF', 1, 0, '', 'hp', ''), # pylint: disable=bad-whitespace | ||
| 199 | - ('QUOTE', 1, 0, '', '', 'datetime'), # pylint: disable=bad-whitespace | ||
| 200 | - ('STYLEREF', 1, 0, '', 'lnprtw', ''), # pylint: disable=bad-whitespace | 197 | + ('NOTEREF', 1, 0, '', 'fhp', ''), |
| 198 | + ('PAGEREF', 1, 0, '', 'hp', ''), | ||
| 199 | + ('QUOTE', 1, 0, '', '', 'datetime'), | ||
| 200 | + ('STYLEREF', 1, 0, '', 'lnprtw', ''), | ||
| 201 | # exclude all Mail Merge commands since they import data from other files | 201 | # exclude all Mail Merge commands since they import data from other files |
| 202 | # (ADDRESSBLOCK, ASK, COMPARE, DATABASE, FILLIN, GREETINGLINE, IF, | 202 | # (ADDRESSBLOCK, ASK, COMPARE, DATABASE, FILLIN, GREETINGLINE, IF, |
| 203 | # MERGEFIELD, MERGEREC, MERGESEQ, NEXT, NEXTIF, SET, SKIPIF) | 203 | # MERGEFIELD, MERGEREC, MERGESEQ, NEXT, NEXTIF, SET, SKIPIF) |
| 204 | # Numbering | 204 | # Numbering |
| 205 | - ('LISTNUM', 0, 1, 'ls', '', ''), # pylint: disable=bad-whitespace | ||
| 206 | - ('PAGE', 0, 0, '', '', 'numeric'), # pylint: disable=bad-whitespace | ||
| 207 | - ('REVNUM', 0, 0, '', '', ''), # pylint: disable=bad-whitespace | ||
| 208 | - ('SECTION', 0, 0, '', '', 'numeric'), # pylint: disable=bad-whitespace | ||
| 209 | - ('SECTIONPAGES', 0, 0, '', '', 'numeric'), # pylint: disable=bad-whitespace | ||
| 210 | - ('SEQ', 1, 1, 'rs', 'chn', 'numeric'), # pylint: disable=bad-whitespace | ||
| 211 | - # user information # pylint: disable=bad-whitespace | ||
| 212 | - ('USERADDRESS', 0, 1, '', '', 'string'), # pylint: disable=bad-whitespace | ||
| 213 | - ('USERINITIALS', 0, 1, '', '', 'string'), # pylint: disable=bad-whitespace | ||
| 214 | - ('USERNAME', 0, 1, '', '', 'string'), # pylint: disable=bad-whitespace | 205 | + ('LISTNUM', 0, 1, 'ls', '', ''), |
| 206 | + ('PAGE', 0, 0, '', '', 'numeric'), | ||
| 207 | + ('REVNUM', 0, 0, '', '', ''), | ||
| 208 | + ('SECTION', 0, 0, '', '', 'numeric'), | ||
| 209 | + ('SECTIONPAGES', 0, 0, '', '', 'numeric'), | ||
| 210 | + ('SEQ', 1, 1, 'rs', 'chn', 'numeric'), | ||
| 211 | + # user information | ||
| 212 | + ('USERADDRESS', 0, 1, '', '', 'string'), | ||
| 213 | + ('USERINITIALS', 0, 1, '', '', 'string'), | ||
| 214 | + ('USERNAME', 0, 1, '', '', 'string'), | ||
| 215 | ) | 215 | ) |
| 216 | 216 | ||
| 217 | FIELD_DDE_REGEX = re.compile(r'^\s*dde(auto)?\s+', re.I) | 217 | FIELD_DDE_REGEX = re.compile(r'^\s*dde(auto)?\s+', re.I) |
oletools/oleobj.py
| @@ -180,7 +180,7 @@ if sys.version_info[0] <= 2: | @@ -180,7 +180,7 @@ if sys.version_info[0] <= 2: | ||
| 180 | NULL_CHAR = '\x00' | 180 | NULL_CHAR = '\x00' |
| 181 | else: | 181 | else: |
| 182 | # Python 3.x | 182 | # Python 3.x |
| 183 | - NULL_CHAR = 0 # pylint: disable=redefined-variable-type | 183 | + NULL_CHAR = 0 |
| 184 | xrange = range # pylint: disable=redefined-builtin, invalid-name | 184 | xrange = range # pylint: disable=redefined-builtin, invalid-name |
| 185 | 185 | ||
| 186 | OOXML_RELATIONSHIP_TAG = '{http://schemas.openxmlformats.org/package/2006/relationships}Relationship' | 186 | OOXML_RELATIONSHIP_TAG = '{http://schemas.openxmlformats.org/package/2006/relationships}Relationship' |
oletools/olevba.py
| @@ -3104,7 +3104,7 @@ class VBA_Parser(object): | @@ -3104,7 +3104,7 @@ class VBA_Parser(object): | ||
| 3104 | log.info('Check whether OLE file is PPT') | 3104 | log.info('Check whether OLE file is PPT') |
| 3105 | try: | 3105 | try: |
| 3106 | ppt = ppt_parser.PptParser(self.ole_file, fast_fail=True) | 3106 | ppt = ppt_parser.PptParser(self.ole_file, fast_fail=True) |
| 3107 | - for vba_data in ppt.iter_vba_data(): | 3107 | + for vba_data in ppt.iter_vba_data(): # pylint: disable=no-value-for-parameter |
| 3108 | self.append_subfile(None, vba_data, container='PptParser') | 3108 | self.append_subfile(None, vba_data, container='PptParser') |
| 3109 | log.info('File is PPT') | 3109 | log.info('File is PPT') |
| 3110 | self.ole_file.close() # just in case | 3110 | self.ole_file.close() # just in case |
oletools/ooxml.py
| @@ -160,7 +160,7 @@ def debug_str(elem): | @@ -160,7 +160,7 @@ def debug_str(elem): | ||
| 160 | def isstr(some_var): | 160 | def isstr(some_var): |
| 161 | """ version-independent test for isinstance(some_var, (str, unicode)) """ | 161 | """ version-independent test for isinstance(some_var, (str, unicode)) """ |
| 162 | if sys.version_info.major == 2: | 162 | if sys.version_info.major == 2: |
| 163 | - return isinstance(some_var, basestring) # true for str and unicode | 163 | + return isinstance(some_var, basestring) # true for str and unicode # pylint: disable=undefined-variable |
| 164 | return isinstance(some_var, str) # there is no unicode | 164 | return isinstance(some_var, str) # there is no unicode |
| 165 | 165 | ||
| 166 | 166 |
oletools/ppt_parser.py
| @@ -1377,7 +1377,7 @@ class PptParser(object): | @@ -1377,7 +1377,7 @@ class PptParser(object): | ||
| 1377 | # first identified in step 3 of Part 1, that is, the UserEditAtom | 1377 | # first identified in step 3 of Part 1, that is, the UserEditAtom |
| 1378 | # record closest to the end of the stream. | 1378 | # record closest to the end of the stream. |
| 1379 | if self.persist_object_directory is None: | 1379 | if self.persist_object_directory is None: |
| 1380 | - self.parse_persist_object_directory() | 1380 | + self.parse_persist_object_directory() # pylint: disable=no-value-for-parameter |
| 1381 | 1381 | ||
| 1382 | # Step 2: Lookup the value of the docPersistIdRef field in the persist | 1382 | # Step 2: Lookup the value of the docPersistIdRef field in the persist |
| 1383 | # object directory constructed in step 8 of Part 1 to find the stream | 1383 | # object directory constructed in step 8 of Part 1 to find the stream |
| @@ -1462,7 +1462,7 @@ class PptParser(object): | @@ -1462,7 +1462,7 @@ class PptParser(object): | ||
| 1462 | rec_len=VBAInfoAtom.RECORD_LENGTH) | 1462 | rec_len=VBAInfoAtom.RECORD_LENGTH) |
| 1463 | 1463 | ||
| 1464 | # try parse | 1464 | # try parse |
| 1465 | - for idx in self.search_pattern(pattern): | 1465 | + for idx in self.search_pattern(pattern): # pylint: disable=no-value-for-parameter |
| 1466 | # assume that in stream at idx there is a VBAInfoContainer | 1466 | # assume that in stream at idx there is a VBAInfoContainer |
| 1467 | stream.seek(idx) | 1467 | stream.seek(idx) |
| 1468 | log.debug('extracting at idx {0}'.format(idx)) | 1468 | log.debug('extracting at idx {0}'.format(idx)) |
| @@ -1515,7 +1515,7 @@ class PptParser(object): | @@ -1515,7 +1515,7 @@ class PptParser(object): | ||
| 1515 | pattern = obj_type.generate_pattern() | 1515 | pattern = obj_type.generate_pattern() |
| 1516 | 1516 | ||
| 1517 | # try parse | 1517 | # try parse |
| 1518 | - for idx in self.search_pattern(pattern): | 1518 | + for idx in self.search_pattern(pattern): # pylint: disable=no-value-for-parameter |
| 1519 | # assume a ExternalObjectStorage in stream at idx | 1519 | # assume a ExternalObjectStorage in stream at idx |
| 1520 | stream.seek(idx) | 1520 | stream.seek(idx) |
| 1521 | log.debug('extracting at idx {0}'.format(idx)) | 1521 | log.debug('extracting at idx {0}'.format(idx)) |
| @@ -1589,7 +1589,7 @@ class PptParser(object): | @@ -1589,7 +1589,7 @@ class PptParser(object): | ||
| 1589 | 1589 | ||
| 1590 | n_infos = 0 | 1590 | n_infos = 0 |
| 1591 | n_macros = 0 | 1591 | n_macros = 0 |
| 1592 | - for info in self.search_vba_info(): | 1592 | + for info in self.search_vba_info(stream): |
| 1593 | n_infos += 1 | 1593 | n_infos += 1 |
| 1594 | if info.vba_info_atom.f_has_macros > 0: | 1594 | if info.vba_info_atom.f_has_macros > 0: |
| 1595 | n_macros += 1 | 1595 | n_macros += 1 |
| @@ -1597,13 +1597,13 @@ class PptParser(object): | @@ -1597,13 +1597,13 @@ class PptParser(object): | ||
| 1597 | # --> no vba-info, so all storages probably ActiveX or other OLE | 1597 | # --> no vba-info, so all storages probably ActiveX or other OLE |
| 1598 | n_storages = 0 | 1598 | n_storages = 0 |
| 1599 | n_compressed = 0 | 1599 | n_compressed = 0 |
| 1600 | - for storage in self.search_vba_storage(): | 1600 | + for storage in self.search_vba_storage(): # pylint: disable=no-value-for-parameter |
| 1601 | n_storages += 1 | 1601 | n_storages += 1 |
| 1602 | if storage.is_compressed: | 1602 | if storage.is_compressed: |
| 1603 | n_compressed += 1 | 1603 | n_compressed += 1 |
| 1604 | - yield self.decompress_vba_storage(storage) | 1604 | + yield self.decompress_vba_storage(storage) # pylint: disable=no-value-for-parameter |
| 1605 | else: | 1605 | else: |
| 1606 | - yield self.read_vba_storage_data(storage) | 1606 | + yield self.read_vba_storage_data(storage) # pylint: disable=no-value-for-parameter |
| 1607 | 1607 | ||
| 1608 | log.info('found {0} infos ({1} with macros) and {2} storages ' | 1608 | log.info('found {0} infos ({1} with macros) and {2} storages ' |
| 1609 | '({3} compressed)' | 1609 | '({3} compressed)' |
oletools/rtfobj.py
| @@ -337,7 +337,7 @@ if sys.version_info[0] <= 2: | @@ -337,7 +337,7 @@ if sys.version_info[0] <= 2: | ||
| 337 | BACKSLASH = '\\' | 337 | BACKSLASH = '\\' |
| 338 | BRACE_OPEN = '{' | 338 | BRACE_OPEN = '{' |
| 339 | BRACE_CLOSE = '}' | 339 | BRACE_CLOSE = '}' |
| 340 | - UNICODE_TYPE = unicode | 340 | + UNICODE_TYPE = unicode # pylint: disable=undefined-variable |
| 341 | else: | 341 | else: |
| 342 | # Python 3.x - Integers | 342 | # Python 3.x - Integers |
| 343 | BACKSLASH = ord('\\') | 343 | BACKSLASH = ord('\\') |
oletools/xls_parser.py
| @@ -229,46 +229,46 @@ class XlsbStream(record_base.OleRecordStream): | @@ -229,46 +229,46 @@ class XlsbStream(record_base.OleRecordStream): | ||
| 229 | 229 | ||
| 230 | # records that appear often but do not need their own XlsRecord subclass (yet) | 230 | # records that appear often but do not need their own XlsRecord subclass (yet) |
| 231 | FREQUENT_RECORDS = dict([ | 231 | FREQUENT_RECORDS = dict([ |
| 232 | - ( 156, 'BuiltInFnGroupCount'), # pylint: disable=bad-whitespace | ||
| 233 | - (2147, 'BookExt'), # pylint: disable=bad-whitespace | ||
| 234 | - ( 442, 'CodeName'), # pylint: disable=bad-whitespace | ||
| 235 | - ( 66, 'CodePage'), # pylint: disable=bad-whitespace | ||
| 236 | - (4195, 'Dat'), # pylint: disable=bad-whitespace | ||
| 237 | - (2154, 'DataLabExt'), # pylint: disable=bad-whitespace | ||
| 238 | - (2155, 'DataLabExtContents'), # pylint: disable=bad-whitespace | ||
| 239 | - ( 215, 'DBCell'), # pylint: disable=bad-whitespace | ||
| 240 | - ( 220, 'DbOrParmQry'), # pylint: disable=bad-whitespace | ||
| 241 | - (2051, 'DBQueryExt'), # pylint: disable=bad-whitespace | ||
| 242 | - (2166, 'DConn'), # pylint: disable=bad-whitespace | ||
| 243 | - ( 35, 'ExternName'), # pylint: disable=bad-whitespace | ||
| 244 | - ( 23, 'ExternSheet'), # pylint: disable=bad-whitespace | ||
| 245 | - ( 255, 'ExtSST'), # pylint: disable=bad-whitespace | ||
| 246 | - (2052, 'ExtString'), # pylint: disable=bad-whitespace | ||
| 247 | - (2151, 'FeatHdr'), # pylint: disable=bad-whitespace | ||
| 248 | - ( 91, 'FileSharing'), # pylint: disable=bad-whitespace | ||
| 249 | - (1054, 'Format'), # pylint: disable=bad-whitespace | ||
| 250 | - ( 49, 'Font'), # pylint: disable=bad-whitespace | ||
| 251 | - (2199, 'GUIDTypeLib'), # pylint: disable=bad-whitespace | ||
| 252 | - ( 440, 'HLink'), # pylint: disable=bad-whitespace | ||
| 253 | - ( 225, 'InterfaceHdr'), # pylint: disable=bad-whitespace | ||
| 254 | - ( 226, 'InterfaceEnd'), # pylint: disable=bad-whitespace | ||
| 255 | - ( 523, 'Index'), # pylint: disable=bad-whitespace | ||
| 256 | - ( 24, 'Lbl'), # pylint: disable=bad-whitespace | ||
| 257 | - ( 193, 'Mms'), # pylint: disable=bad-whitespace | ||
| 258 | - ( 93, 'Obj'), # pylint: disable=bad-whitespace | ||
| 259 | - (4135, 'ObjectLink'), # pylint: disable=bad-whitespace | ||
| 260 | - (2058, 'OleDbConn'), # pylint: disable=bad-whitespace | ||
| 261 | - ( 222, 'OleObjectSize'), # pylint: disable=bad-whitespace | ||
| 262 | - (2214, 'RichTextStream'), # pylint: disable=bad-whitespace | ||
| 263 | - (2146, 'SheetExt'), # pylint: disable=bad-whitespace | ||
| 264 | - (1212, 'ShrFmla'), # pylint: disable=bad-whitespace | ||
| 265 | - (2060, 'SxViewExt'), # pylint: disable=bad-whitespace | ||
| 266 | - (2136, 'SxViewLink'), # pylint: disable=bad-whitespace | ||
| 267 | - (2049, 'WebPub'), # pylint: disable=bad-whitespace | ||
| 268 | - ( 224, 'XF (formatting)'), # pylint: disable=bad-whitespace | ||
| 269 | - (2173, 'XFExt (formatting)'), # pylint: disable=bad-whitespace | ||
| 270 | - ( 659, 'Style'), # pylint: disable=bad-whitespace | ||
| 271 | - (2194, 'StyleExt') # pylint: disable=bad-whitespace | 232 | + ( 156, 'BuiltInFnGroupCount'), |
| 233 | + (2147, 'BookExt'), | ||
| 234 | + ( 442, 'CodeName'), | ||
| 235 | + ( 66, 'CodePage'), | ||
| 236 | + (4195, 'Dat'), | ||
| 237 | + (2154, 'DataLabExt'), | ||
| 238 | + (2155, 'DataLabExtContents'), | ||
| 239 | + ( 215, 'DBCell'), | ||
| 240 | + ( 220, 'DbOrParmQry'), | ||
| 241 | + (2051, 'DBQueryExt'), | ||
| 242 | + (2166, 'DConn'), | ||
| 243 | + ( 35, 'ExternName'), | ||
| 244 | + ( 23, 'ExternSheet'), | ||
| 245 | + ( 255, 'ExtSST'), | ||
| 246 | + (2052, 'ExtString'), | ||
| 247 | + (2151, 'FeatHdr'), | ||
| 248 | + ( 91, 'FileSharing'), | ||
| 249 | + (1054, 'Format'), | ||
| 250 | + ( 49, 'Font'), | ||
| 251 | + (2199, 'GUIDTypeLib'), | ||
| 252 | + ( 440, 'HLink'), | ||
| 253 | + ( 225, 'InterfaceHdr'), | ||
| 254 | + ( 226, 'InterfaceEnd'), | ||
| 255 | + ( 523, 'Index'), | ||
| 256 | + ( 24, 'Lbl'), | ||
| 257 | + ( 193, 'Mms'), | ||
| 258 | + ( 93, 'Obj'), | ||
| 259 | + (4135, 'ObjectLink'), | ||
| 260 | + (2058, 'OleDbConn'), | ||
| 261 | + ( 222, 'OleObjectSize'), | ||
| 262 | + (2214, 'RichTextStream'), | ||
| 263 | + (2146, 'SheetExt'), | ||
| 264 | + (1212, 'ShrFmla'), | ||
| 265 | + (2060, 'SxViewExt'), | ||
| 266 | + (2136, 'SxViewLink'), | ||
| 267 | + (2049, 'WebPub'), | ||
| 268 | + ( 224, 'XF (formatting)'), | ||
| 269 | + (2173, 'XFExt (formatting)'), | ||
| 270 | + ( 659, 'Style'), | ||
| 271 | + (2194, 'StyleExt') | ||
| 272 | ]) | 272 | ]) |
| 273 | 273 | ||
| 274 | #: records found in xlsb binary parts | 274 | #: records found in xlsb binary parts |
tests/common/log_helper/log_helper_test_imported.py
| @@ -4,6 +4,7 @@ by the main test file | @@ -4,6 +4,7 @@ by the main test file | ||
| 4 | """ | 4 | """ |
| 5 | 5 | ||
| 6 | from oletools.common.log_helper import log_helper | 6 | from oletools.common.log_helper import log_helper |
| 7 | +import warnings | ||
| 7 | 8 | ||
| 8 | DEBUG_MESSAGE = 'imported: debug log' | 9 | DEBUG_MESSAGE = 'imported: debug log' |
| 9 | INFO_MESSAGE = 'imported: info log' | 10 | INFO_MESSAGE = 'imported: info log' |
| @@ -11,7 +12,10 @@ WARNING_MESSAGE = 'imported: warning log' | @@ -11,7 +12,10 @@ WARNING_MESSAGE = 'imported: warning log' | ||
| 11 | ERROR_MESSAGE = 'imported: error log' | 12 | ERROR_MESSAGE = 'imported: error log' |
| 12 | CRITICAL_MESSAGE = 'imported: critical log' | 13 | CRITICAL_MESSAGE = 'imported: critical log' |
| 13 | RESULT_MESSAGE = 'imported: result log' | 14 | RESULT_MESSAGE = 'imported: result log' |
| 15 | + | ||
| 14 | RESULT_TYPE = 'imported: result' | 16 | RESULT_TYPE = 'imported: result' |
| 17 | +ACTUAL_WARNING = 'Feature XYZ provided by this module might be deprecated at '\ | ||
| 18 | + 'some point in the future ... or not' | ||
| 15 | 19 | ||
| 16 | logger = log_helper.get_or_create_silent_logger('test_imported') | 20 | logger = log_helper.get_or_create_silent_logger('test_imported') |
| 17 | 21 | ||
| @@ -27,3 +31,7 @@ def log(): | @@ -27,3 +31,7 @@ def log(): | ||
| 27 | logger.error(ERROR_MESSAGE) | 31 | logger.error(ERROR_MESSAGE) |
| 28 | logger.critical(CRITICAL_MESSAGE) | 32 | logger.critical(CRITICAL_MESSAGE) |
| 29 | logger.info(RESULT_MESSAGE, type=RESULT_TYPE) | 33 | logger.info(RESULT_MESSAGE, type=RESULT_TYPE) |
| 34 | + | ||
| 35 | + | ||
| 36 | +def warn(): | ||
| 37 | + warnings.warn(ACTUAL_WARNING) |
tests/common/log_helper/log_helper_test_main.py
| @@ -2,6 +2,7 @@ | @@ -2,6 +2,7 @@ | ||
| 2 | 2 | ||
| 3 | import sys | 3 | import sys |
| 4 | import logging | 4 | import logging |
| 5 | +import warnings | ||
| 5 | from tests.common.log_helper import log_helper_test_imported | 6 | from tests.common.log_helper import log_helper_test_imported |
| 6 | from oletools.common.log_helper import log_helper | 7 | from oletools.common.log_helper import log_helper |
| 7 | 8 | ||
| @@ -11,7 +12,9 @@ WARNING_MESSAGE = 'main: warning log' | @@ -11,7 +12,9 @@ WARNING_MESSAGE = 'main: warning log' | ||
| 11 | ERROR_MESSAGE = 'main: error log' | 12 | ERROR_MESSAGE = 'main: error log' |
| 12 | CRITICAL_MESSAGE = 'main: critical log' | 13 | CRITICAL_MESSAGE = 'main: critical log' |
| 13 | RESULT_MESSAGE = 'main: result log' | 14 | RESULT_MESSAGE = 'main: result log' |
| 15 | + | ||
| 14 | RESULT_TYPE = 'main: result' | 16 | RESULT_TYPE = 'main: result' |
| 17 | +ACTUAL_WARNING = 'Warnings can pop up anywhere, have to be prepared!' | ||
| 15 | 18 | ||
| 16 | logger = log_helper.get_or_create_silent_logger('test_main') | 19 | logger = log_helper.get_or_create_silent_logger('test_main') |
| 17 | 20 | ||
| @@ -24,7 +27,8 @@ def enable_logging(): | @@ -24,7 +27,8 @@ def enable_logging(): | ||
| 24 | 27 | ||
| 25 | def main(args): | 28 | def main(args): |
| 26 | """ | 29 | """ |
| 27 | - Try to cover possible logging scenarios. For each scenario covered, here's the expected args and outcome: | 30 | + Try to cover possible logging scenarios. For each scenario covered, here's |
| 31 | + the expected args and outcome: | ||
| 28 | - Log without enabling: ['<level>'] | 32 | - Log without enabling: ['<level>'] |
| 29 | * logging when being imported - should never print | 33 | * logging when being imported - should never print |
| 30 | - Log as JSON without enabling: ['as-json', '<level>'] | 34 | - Log as JSON without enabling: ['as-json', '<level>'] |
| @@ -35,6 +39,8 @@ def main(args): | @@ -35,6 +39,8 @@ def main(args): | ||
| 35 | * logging as JSON when being run as script - should log messages as JSON | 39 | * logging as JSON when being run as script - should log messages as JSON |
| 36 | - Enable, log as JSON and throw: ['enable', 'as-json', 'throw', '<level>'] | 40 | - Enable, log as JSON and throw: ['enable', 'as-json', 'throw', '<level>'] |
| 37 | * should produce JSON-compatible output, even after an unhandled exception | 41 | * should produce JSON-compatible output, even after an unhandled exception |
| 42 | + - Enable, log as JSON and warn: ['enable', 'as-json', 'warn', '<level>'] | ||
| 43 | + * should produce JSON-compatible output, even after a warning | ||
| 38 | """ | 44 | """ |
| 39 | 45 | ||
| 40 | # the level should always be the last argument passed | 46 | # the level should always be the last argument passed |
| @@ -42,6 +48,9 @@ def main(args): | @@ -42,6 +48,9 @@ def main(args): | ||
| 42 | use_json = 'as-json' in args | 48 | use_json = 'as-json' in args |
| 43 | throw = 'throw' in args | 49 | throw = 'throw' in args |
| 44 | percent_autoformat = '%-autoformat' in args | 50 | percent_autoformat = '%-autoformat' in args |
| 51 | + warn = 'warn' in args | ||
| 52 | + exc_info = 'exc-info' in args | ||
| 53 | + wrong_log_args = 'wrong-log-args' in args | ||
| 45 | 54 | ||
| 46 | log_helper_test_imported.logger.setLevel(logging.ERROR) | 55 | log_helper_test_imported.logger.setLevel(logging.ERROR) |
| 47 | 56 | ||
| @@ -53,6 +62,22 @@ def main(args): | @@ -53,6 +62,22 @@ def main(args): | ||
| 53 | if throw: | 62 | if throw: |
| 54 | raise Exception('An exception occurred before ending the logging') | 63 | raise Exception('An exception occurred before ending the logging') |
| 55 | 64 | ||
| 65 | + if warn: | ||
| 66 | + warnings.warn(ACTUAL_WARNING) | ||
| 67 | + log_helper_test_imported.warn() | ||
| 68 | + | ||
| 69 | + if exc_info: | ||
| 70 | + try: | ||
| 71 | + raise Exception('This is an exception') | ||
| 72 | + except Exception: | ||
| 73 | + logger.exception('Caught exception') # has exc_info=True | ||
| 74 | + | ||
| 75 | + if wrong_log_args: | ||
| 76 | + logger.info('Opening file /dangerous/file/with-%s-in-name') | ||
| 77 | + logger.info('The result is %f') | ||
| 78 | + logger.info('No result', 1.23) | ||
| 79 | + logger.info('The result is %f', 'bla') | ||
| 80 | + | ||
| 56 | log_helper.end_logging() | 81 | log_helper.end_logging() |
| 57 | 82 | ||
| 58 | 83 |
tests/common/log_helper/test_log_helper.py
| @@ -11,13 +11,13 @@ import json | @@ -11,13 +11,13 @@ import json | ||
| 11 | import subprocess | 11 | import subprocess |
| 12 | from tests.common.log_helper import log_helper_test_main | 12 | from tests.common.log_helper import log_helper_test_main |
| 13 | from tests.common.log_helper import log_helper_test_imported | 13 | from tests.common.log_helper import log_helper_test_imported |
| 14 | +import os | ||
| 14 | from os.path import dirname, join, relpath, abspath | 15 | from os.path import dirname, join, relpath, abspath |
| 15 | 16 | ||
| 16 | from tests.test_utils import PROJECT_ROOT | 17 | from tests.test_utils import PROJECT_ROOT |
| 17 | 18 | ||
| 18 | # test file we use as "main" module | 19 | # test file we use as "main" module |
| 19 | -TEST_FILE = relpath(join(dirname(abspath(__file__)), 'log_helper_test_main.py'), | ||
| 20 | - PROJECT_ROOT) | 20 | +TEST_FILE = join(dirname(abspath(__file__)), 'log_helper_test_main.py') |
| 21 | 21 | ||
| 22 | # test file simulating a third party main module that only imports oletools | 22 | # test file simulating a third party main module that only imports oletools |
| 23 | TEST_FILE_3RD_PARTY = relpath(join(dirname(abspath(__file__)), | 23 | TEST_FILE_3RD_PARTY = relpath(join(dirname(abspath(__file__)), |
| @@ -26,6 +26,8 @@ TEST_FILE_3RD_PARTY = relpath(join(dirname(abspath(__file__)), | @@ -26,6 +26,8 @@ TEST_FILE_3RD_PARTY = relpath(join(dirname(abspath(__file__)), | ||
| 26 | 26 | ||
| 27 | PYTHON_EXECUTABLE = sys.executable | 27 | PYTHON_EXECUTABLE = sys.executable |
| 28 | 28 | ||
| 29 | +PERCENT_FORMAT_OUTPUT = 'The answer is 47.' | ||
| 30 | + | ||
| 29 | 31 | ||
| 30 | class TestLogHelper(unittest.TestCase): | 32 | class TestLogHelper(unittest.TestCase): |
| 31 | def test_it_doesnt_log_when_not_enabled(self): | 33 | def test_it_doesnt_log_when_not_enabled(self): |
| @@ -113,7 +115,7 @@ class TestLogHelper(unittest.TestCase): | @@ -113,7 +115,7 @@ class TestLogHelper(unittest.TestCase): | ||
| 113 | def test_percent_autoformat(self): | 115 | def test_percent_autoformat(self): |
| 114 | """Test that auto-formatting of log strings with `%` works.""" | 116 | """Test that auto-formatting of log strings with `%` works.""" |
| 115 | output = self._run_test(['enable', '%-autoformat', 'info']) | 117 | output = self._run_test(['enable', '%-autoformat', 'info']) |
| 116 | - self.assertIn('The answer is 47.', output) | 118 | + self.assertIn(PERCENT_FORMAT_OUTPUT, output) |
| 117 | 119 | ||
| 118 | def test_json_correct_on_exceptions(self): | 120 | def test_json_correct_on_exceptions(self): |
| 119 | """ | 121 | """ |
| @@ -141,6 +143,93 @@ class TestLogHelper(unittest.TestCase): | @@ -141,6 +143,93 @@ class TestLogHelper(unittest.TestCase): | ||
| 141 | self.assertIn('INFO:test_main:main: info log', output) | 143 | self.assertIn('INFO:test_main:main: info log', output) |
| 142 | self.assertIn('INFO:test_imported:imported: info log', output) | 144 | self.assertIn('INFO:test_imported:imported: info log', output) |
| 143 | 145 | ||
| 146 | + def test_json_correct_on_warnings(self): | ||
| 147 | + """ | ||
| 148 | + Test that even on warnings our JSON is always correct | ||
| 149 | + """ | ||
| 150 | + output = self._run_test(['enable', 'as-json', 'warn', 'warning']) | ||
| 151 | + expected_messages = [ | ||
| 152 | + log_helper_test_main.WARNING_MESSAGE, | ||
| 153 | + log_helper_test_main.ERROR_MESSAGE, | ||
| 154 | + log_helper_test_main.CRITICAL_MESSAGE, | ||
| 155 | + log_helper_test_imported.WARNING_MESSAGE, | ||
| 156 | + log_helper_test_imported.ERROR_MESSAGE, | ||
| 157 | + log_helper_test_imported.CRITICAL_MESSAGE, | ||
| 158 | + ] | ||
| 159 | + | ||
| 160 | + for msg in expected_messages: | ||
| 161 | + self.assertIn(msg, output) | ||
| 162 | + | ||
| 163 | + # last two entries of output should be warnings | ||
| 164 | + jout = json.loads(output) | ||
| 165 | + self.assertEqual(jout[-2]['level'], 'WARNING') | ||
| 166 | + self.assertEqual(jout[-1]['level'], 'WARNING') | ||
| 167 | + self.assertEqual(jout[-2]['type'], 'warning') | ||
| 168 | + self.assertEqual(jout[-1]['type'], 'warning') | ||
| 169 | + self.assertIn(log_helper_test_main.ACTUAL_WARNING, jout[-2]['msg']) | ||
| 170 | + self.assertIn(log_helper_test_imported.ACTUAL_WARNING, jout[-1]['msg']) | ||
| 171 | + | ||
| 172 | + def test_warnings(self): | ||
| 173 | + """Check that warnings are captured and printed correctly""" | ||
| 174 | + output = self._run_test(['enable', 'warn', 'warning']) | ||
| 175 | + | ||
| 176 | + # find out which line contains the call to warnings.warn: | ||
| 177 | + warnings_line = None | ||
| 178 | + with open(TEST_FILE, 'rt') as reader: | ||
| 179 | + for line_idx, line in enumerate(reader): | ||
| 180 | + if 'warnings.warn' in line: | ||
| 181 | + warnings_line = line_idx + 1 | ||
| 182 | + break | ||
| 183 | + self.assertNotEqual(warnings_line, None) | ||
| 184 | + | ||
| 185 | + imported_file = join(dirname(abspath(__file__)), | ||
| 186 | + 'log_helper_test_imported.py') | ||
| 187 | + imported_line = None | ||
| 188 | + with open(imported_file, 'rt') as reader: | ||
| 189 | + for line_idx, line in enumerate(reader): | ||
| 190 | + if 'warnings.warn' in line: | ||
| 191 | + imported_line = line_idx + 1 | ||
| 192 | + break | ||
| 193 | + self.assertNotEqual(imported_line, None) | ||
| 194 | + | ||
| 195 | + expect = '\n'.join([ | ||
| 196 | + 'WARNING ' + log_helper_test_main.WARNING_MESSAGE, | ||
| 197 | + 'ERROR ' + log_helper_test_main.ERROR_MESSAGE, | ||
| 198 | + 'CRITICAL ' + log_helper_test_main.CRITICAL_MESSAGE, | ||
| 199 | + 'WARNING ' + log_helper_test_imported.WARNING_MESSAGE, | ||
| 200 | + 'ERROR ' + log_helper_test_imported.ERROR_MESSAGE, | ||
| 201 | + 'CRITICAL ' + log_helper_test_imported.CRITICAL_MESSAGE, | ||
| 202 | + 'WARNING {0}:{1}: UserWarning: {2}' | ||
| 203 | + .format(TEST_FILE, warnings_line, log_helper_test_main.ACTUAL_WARNING), | ||
| 204 | + ' warnings.warn(ACTUAL_WARNING)', # warnings include source line | ||
| 205 | + '', | ||
| 206 | + 'WARNING {0}:{1}: UserWarning: {2}' | ||
| 207 | + .format(imported_file, imported_line, log_helper_test_imported.ACTUAL_WARNING), | ||
| 208 | + ' warnings.warn(ACTUAL_WARNING)', # warnings include source line | ||
| 209 | + ]) | ||
| 210 | + self.assertEqual(output.strip(), expect) | ||
| 211 | + | ||
| 212 | + def test_json_percent_formatting(self): | ||
| 213 | + """Test that json-output has formatting args included in output.""" | ||
| 214 | + output = self._run_test(['enable', 'as-json', '%-autoformat', 'info']) | ||
| 215 | + json.loads(output) # check that this does not raise, so json is valid | ||
| 216 | + self.assertIn(PERCENT_FORMAT_OUTPUT, output) | ||
| 217 | + | ||
| 218 | + def test_json_exception_formatting(self): | ||
| 219 | + """Test that json-output has formatted exception info in output""" | ||
| 220 | + output = self._run_test(['enable', 'as-json', 'exc-info', 'info']) | ||
| 221 | + json.loads(output) # check that this does not raise, so json is valid | ||
| 222 | + self.assertIn('Caught exception', output) # actual log message | ||
| 223 | + self.assertIn('This is an exception', output) # message of caught exception | ||
| 224 | + self.assertIn('Traceback (most recent call last)', output) # start of trace | ||
| 225 | + self.assertIn(TEST_FILE.replace('\\', '\\\\'), output) # part of trace | ||
| 226 | + | ||
| 227 | + def test_json_wrong_args(self): | ||
| 228 | + """Test that too many or missing args do not raise exceptions inside logger""" | ||
| 229 | + output = self._run_test(['enable', 'as-json', 'wrong-log-args', 'info']) | ||
| 230 | + json.loads(output) # check that this does not raise, so json is valid | ||
| 231 | + # do not care about actual contents of output | ||
| 232 | + | ||
| 144 | def _assert_json_messages(self, output, messages): | 233 | def _assert_json_messages(self, output, messages): |
| 145 | try: | 234 | try: |
| 146 | json_data = json.loads(output) | 235 | json_data = json.loads(output) |
| @@ -160,8 +249,10 @@ class TestLogHelper(unittest.TestCase): | @@ -160,8 +249,10 @@ class TestLogHelper(unittest.TestCase): | ||
| 160 | we might get errors or false positives between sequential tests runs) | 249 | we might get errors or false positives between sequential tests runs) |
| 161 | 250 | ||
| 162 | When arg `run_third_party` is `True`, we do not run the `TEST_FILE` as | 251 | When arg `run_third_party` is `True`, we do not run the `TEST_FILE` as |
| 163 | - main moduel but the `TEST_FILE_3RD_PARTY` and return contents of | 252 | + main module but the `TEST_FILE_3RD_PARTY` and return contents of |
| 164 | `stderr` instead of `stdout`. | 253 | `stderr` instead of `stdout`. |
| 254 | + | ||
| 255 | + TODO: use tests.utils.call_and_capture | ||
| 165 | """ | 256 | """ |
| 166 | all_args = [PYTHON_EXECUTABLE, ] | 257 | all_args = [PYTHON_EXECUTABLE, ] |
| 167 | if run_third_party: | 258 | if run_third_party: |
| @@ -169,10 +260,12 @@ class TestLogHelper(unittest.TestCase): | @@ -169,10 +260,12 @@ class TestLogHelper(unittest.TestCase): | ||
| 169 | else: | 260 | else: |
| 170 | all_args.append(TEST_FILE) | 261 | all_args.append(TEST_FILE) |
| 171 | all_args.extend(args) | 262 | all_args.extend(args) |
| 263 | + env = os.environ.copy() | ||
| 264 | + env['PYTHONPATH'] = PROJECT_ROOT | ||
| 172 | child = subprocess.Popen( | 265 | child = subprocess.Popen( |
| 173 | all_args, | 266 | all_args, |
| 174 | shell=False, | 267 | shell=False, |
| 175 | - env={'PYTHONPATH': PROJECT_ROOT}, | 268 | + env=env, |
| 176 | universal_newlines=True, | 269 | universal_newlines=True, |
| 177 | cwd=PROJECT_ROOT, | 270 | cwd=PROJECT_ROOT, |
| 178 | stdin=None, | 271 | stdin=None, |
tests/common/test_encoding_handler.py
| @@ -14,7 +14,13 @@ FILE_TEXT = u'The unicode check mark is \u2713.\n' | @@ -14,7 +14,13 @@ FILE_TEXT = u'The unicode check mark is \u2713.\n' | ||
| 14 | 14 | ||
| 15 | @contextmanager | 15 | @contextmanager |
| 16 | def temp_file(just_name=True): | 16 | def temp_file(just_name=True): |
| 17 | - """Context manager that creates temp file and deletes it in the end""" | 17 | + """ |
| 18 | + Context manager that creates temp file and deletes it in the end. | ||
| 19 | + | ||
| 20 | + If `just_name` is `False` this yields (file-name, open-file-handle), | ||
| 21 | + if `just_name` is `True` this yields just the file-name (and closes | ||
| 22 | + the file-handle if we are on windows) | ||
| 23 | + """ | ||
| 18 | tmp_descriptor = None | 24 | tmp_descriptor = None |
| 19 | tmp_name = None | 25 | tmp_name = None |
| 20 | tmp_handle = None | 26 | tmp_handle = None |
| @@ -24,8 +30,12 @@ def temp_file(just_name=True): | @@ -24,8 +30,12 @@ def temp_file(just_name=True): | ||
| 24 | # we create our own file handle since we want to be able to close the | 30 | # we create our own file handle since we want to be able to close the |
| 25 | # file and open it again for reading. | 31 | # file and open it again for reading. |
| 26 | # We keep the os-level descriptor open so file name is still reserved | 32 | # We keep the os-level descriptor open so file name is still reserved |
| 27 | - # for us | 33 | + # for us ... except for Windows where it is not possible for another |
| 34 | + # process to write to that handle | ||
| 28 | if just_name: | 35 | if just_name: |
| 36 | + if sys.platform.startswith('win'): | ||
| 37 | + os.close(tmp_descriptor) | ||
| 38 | + tmp_descriptor = None | ||
| 29 | yield tmp_name | 39 | yield tmp_name |
| 30 | else: | 40 | else: |
| 31 | tmp_handle = open(tmp_name, 'wb') | 41 | tmp_handle = open(tmp_name, 'wb') |
| @@ -51,11 +61,7 @@ class TestEncodingHandler(unittest.TestCase): | @@ -51,11 +61,7 @@ class TestEncodingHandler(unittest.TestCase): | ||
| 51 | shell=True) | 61 | shell=True) |
| 52 | 62 | ||
| 53 | def test_print_redirect(self): | 63 | def test_print_redirect(self): |
| 54 | - """ | ||
| 55 | - Test redirection of unicode output to files does not raise error | ||
| 56 | - | ||
| 57 | - TODO: test this on non-linux OSs | ||
| 58 | - """ | 64 | + """Test redirection of unicode output to files does not raise error.""" |
| 59 | with temp_file() as tmp_file: | 65 | with temp_file() as tmp_file: |
| 60 | check_call('{python} {this_file} print > {tmp_file}' | 66 | check_call('{python} {this_file} print > {tmp_file}' |
| 61 | .format(python=sys.executable, this_file=__file__, | 67 | .format(python=sys.executable, this_file=__file__, |
| @@ -63,7 +69,7 @@ class TestEncodingHandler(unittest.TestCase): | @@ -63,7 +69,7 @@ class TestEncodingHandler(unittest.TestCase): | ||
| 63 | shell=True) | 69 | shell=True) |
| 64 | 70 | ||
| 65 | @unittest.skipIf(not sys.platform.startswith('linux'), | 71 | @unittest.skipIf(not sys.platform.startswith('linux'), |
| 66 | - 'Only tested on linux sofar') | 72 | + 'Need to adapt this test to Windows') |
| 67 | def test_print_no_lang(self): | 73 | def test_print_no_lang(self): |
| 68 | """ | 74 | """ |
| 69 | Test redirection of unicode output to files does not raise error | 75 | Test redirection of unicode output to files does not raise error |
| @@ -89,11 +95,7 @@ class TestEncodingHandler(unittest.TestCase): | @@ -89,11 +95,7 @@ class TestEncodingHandler(unittest.TestCase): | ||
| 89 | self.fail(cpe.output) | 95 | self.fail(cpe.output) |
| 90 | 96 | ||
| 91 | def test_uopen_redirect(self): | 97 | def test_uopen_redirect(self): |
| 92 | - """ | ||
| 93 | - Test redirection of unicode output to files does not raise error | ||
| 94 | - | ||
| 95 | - TODO: test this on non-linux OSs | ||
| 96 | - """ | 98 | + """Test redirection of unicode output to files does not raise error.""" |
| 97 | with temp_file(False) as (tmp_handle, tmp_file): | 99 | with temp_file(False) as (tmp_handle, tmp_file): |
| 98 | tmp_handle.write(FILE_TEXT.encode('utf8')) | 100 | tmp_handle.write(FILE_TEXT.encode('utf8')) |
| 99 | tmp_handle.close() | 101 | tmp_handle.close() |
| @@ -109,7 +111,7 @@ class TestEncodingHandler(unittest.TestCase): | @@ -109,7 +111,7 @@ class TestEncodingHandler(unittest.TestCase): | ||
| 109 | self.fail(cpe.output) | 111 | self.fail(cpe.output) |
| 110 | 112 | ||
| 111 | @unittest.skipIf(not sys.platform.startswith('linux'), | 113 | @unittest.skipIf(not sys.platform.startswith('linux'), |
| 112 | - 'Only tested on linux sofar') | 114 | + 'Need to adapt this test to Windows') |
| 113 | def test_uopen_no_lang(self): | 115 | def test_uopen_no_lang(self): |
| 114 | """ | 116 | """ |
| 115 | Test that uopen in a C-LANG environment is ok | 117 | Test that uopen in a C-LANG environment is ok |
tests/ftguess/test_basic.py
| 1 | """Test ftguess""" | 1 | """Test ftguess""" |
| 2 | - | ||
| 3 | import unittest | 2 | import unittest |
| 4 | import os | 3 | import os |
| 5 | -from os.path import splitext | 4 | +from os.path import splitext, join |
| 6 | from oletools import ftguess | 5 | from oletools import ftguess |
| 7 | 6 | ||
| 8 | # Directory with test data, independent of current working directory | 7 | # Directory with test data, independent of current working directory |
| @@ -47,7 +46,7 @@ class TestFTGuess(unittest.TestCase): | @@ -47,7 +46,7 @@ class TestFTGuess(unittest.TestCase): | ||
| 47 | before_dot, extension = splitext(filename) | 46 | before_dot, extension = splitext(filename) |
| 48 | if extension == '.zip': | 47 | if extension == '.zip': |
| 49 | extension = splitext(before_dot)[1] | 48 | extension = splitext(before_dot)[1] |
| 50 | - elif filename in ('basic/empty', 'basic/text'): | 49 | + elif filename in (join('basic', 'empty'), join('basic', 'text')): |
| 51 | extension = '.csv' # have just like that | 50 | extension = '.csv' # have just like that |
| 52 | elif not extension: | 51 | elif not extension: |
| 53 | self.fail('Could not find extension for test sample {0}' | 52 | self.fail('Could not find extension for test sample {0}' |
| @@ -55,7 +54,7 @@ class TestFTGuess(unittest.TestCase): | @@ -55,7 +54,7 @@ class TestFTGuess(unittest.TestCase): | ||
| 55 | extension = extension[1:] # remove the leading '.' | 54 | extension = extension[1:] # remove the leading '.' |
| 56 | 55 | ||
| 57 | # encrypted files are mostly not recognized (yet?), except .xls | 56 | # encrypted files are mostly not recognized (yet?), except .xls |
| 58 | - if filename.startswith('encrypted/'): | 57 | + if filename.startswith('encrypted' + os.sep): |
| 59 | if extension == 'xls': | 58 | if extension == 'xls': |
| 60 | expect = ftguess.FType_Excel97 | 59 | expect = ftguess.FType_Excel97 |
| 61 | else: | 60 | else: |
| @@ -69,7 +68,7 @@ class TestFTGuess(unittest.TestCase): | @@ -69,7 +68,7 @@ class TestFTGuess(unittest.TestCase): | ||
| 69 | # not implemented yet | 68 | # not implemented yet |
| 70 | expect = ftguess.FType_Unknown | 69 | expect = ftguess.FType_Unknown |
| 71 | 70 | ||
| 72 | - elif filename == 'basic/encrypted.docx': | 71 | + elif filename == join('basic', 'encrypted.docx'): |
| 73 | expect = ftguess.FType_Generic_OLE | 72 | expect = ftguess.FType_Generic_OLE |
| 74 | 73 | ||
| 75 | elif 'excel5' in filename: | 74 | elif 'excel5' in filename: |
tests/msodde/test_basic.py
| @@ -9,6 +9,7 @@ Ensure that | @@ -9,6 +9,7 @@ Ensure that | ||
| 9 | from __future__ import print_function | 9 | from __future__ import print_function |
| 10 | 10 | ||
| 11 | import unittest | 11 | import unittest |
| 12 | +from platform import python_implementation | ||
| 12 | import sys | 13 | import sys |
| 13 | import os | 14 | import os |
| 14 | from os.path import join, basename | 15 | from os.path import join, basename |
| @@ -19,8 +20,21 @@ from tests.test_utils import call_and_capture, decrypt_sample,\ | @@ -19,8 +20,21 @@ from tests.test_utils import call_and_capture, decrypt_sample,\ | ||
| 19 | DATA_BASE_DIR as BASE_DIR | 20 | DATA_BASE_DIR as BASE_DIR |
| 20 | 21 | ||
| 21 | 22 | ||
| 23 | +# Check whether we run with PyPy on windows because that causes trouble | ||
| 24 | +# when using the :py:func:`tests.test_utils.decrypt_sample`. | ||
| 25 | +# | ||
| 26 | +# :return: `(do_skip, explanation)` where `do_skip` is `True` iff running | ||
| 27 | +# PyPy on Windows; `explanation` is a simple text string | ||
| 28 | +SKIP_PYPY_WIN = ( | ||
| 29 | + python_implementation().lower().startswith('pypy') | ||
| 30 | + and sys.platform.lower().startswith('win'), | ||
| 31 | + "On PyPy there is a problem with deleting temp files for decrypt_sample" | ||
| 32 | +) | ||
| 33 | + | ||
| 34 | + | ||
| 22 | class TestReturnCode(unittest.TestCase): | 35 | class TestReturnCode(unittest.TestCase): |
| 23 | """ check return codes and exception behaviour (not text output) """ | 36 | """ check return codes and exception behaviour (not text output) """ |
| 37 | + @unittest.skipIf(*SKIP_PYPY_WIN) | ||
| 24 | def test_valid_doc(self): | 38 | def test_valid_doc(self): |
| 25 | """ check that a valid doc file leads to 0 exit status """ | 39 | """ check that a valid doc file leads to 0 exit status """ |
| 26 | for filename in ( | 40 | for filename in ( |
| @@ -44,6 +58,7 @@ class TestReturnCode(unittest.TestCase): | @@ -44,6 +58,7 @@ class TestReturnCode(unittest.TestCase): | ||
| 44 | self.do_test_validity(join(BASE_DIR, 'msodde', | 58 | self.do_test_validity(join(BASE_DIR, 'msodde', |
| 45 | filename + '.docm')) | 59 | filename + '.docm')) |
| 46 | 60 | ||
| 61 | + @unittest.skipIf(*SKIP_PYPY_WIN) | ||
| 47 | def test_valid_xml(self): | 62 | def test_valid_xml(self): |
| 48 | """ check that xml leads to 0 exit status """ | 63 | """ check that xml leads to 0 exit status """ |
| 49 | for filename in ( | 64 | for filename in ( |
| @@ -67,11 +82,11 @@ class TestReturnCode(unittest.TestCase): | @@ -67,11 +82,11 @@ class TestReturnCode(unittest.TestCase): | ||
| 67 | 82 | ||
| 68 | def test_invalid_empty(self): | 83 | def test_invalid_empty(self): |
| 69 | """ check that empty file argument leads to non-zero exit status """ | 84 | """ check that empty file argument leads to non-zero exit status """ |
| 70 | - self.do_test_validity(join(BASE_DIR, 'basic/empty'), Exception) | 85 | + self.do_test_validity(join(BASE_DIR, 'basic', 'empty'), Exception) |
| 71 | 86 | ||
| 72 | def test_invalid_text(self): | 87 | def test_invalid_text(self): |
| 73 | """ check that text file argument leads to non-zero exit status """ | 88 | """ check that text file argument leads to non-zero exit status """ |
| 74 | - self.do_test_validity(join(BASE_DIR, 'basic/text'), Exception) | 89 | + self.do_test_validity(join(BASE_DIR, 'basic', 'text'), Exception) |
| 75 | 90 | ||
| 76 | def test_encrypted(self): | 91 | def test_encrypted(self): |
| 77 | """ | 92 | """ |
| @@ -140,6 +155,7 @@ class TestDdeLinks(unittest.TestCase): | @@ -140,6 +155,7 @@ class TestDdeLinks(unittest.TestCase): | ||
| 140 | """ | 155 | """ |
| 141 | return [o for o in output.splitlines()] | 156 | return [o for o in output.splitlines()] |
| 142 | 157 | ||
| 158 | + @unittest.skipIf(*SKIP_PYPY_WIN) | ||
| 143 | def test_with_dde(self): | 159 | def test_with_dde(self): |
| 144 | """ check that dde links appear on stdout """ | 160 | """ check that dde links appear on stdout """ |
| 145 | filename = 'dde-test-from-office2003.doc.zip' | 161 | filename = 'dde-test-from-office2003.doc.zip' |
| @@ -158,6 +174,7 @@ class TestDdeLinks(unittest.TestCase): | @@ -158,6 +174,7 @@ class TestDdeLinks(unittest.TestCase): | ||
| 158 | self.assertEqual(len(self.get_dde_from_output(output)), 0, | 174 | self.assertEqual(len(self.get_dde_from_output(output)), 0, |
| 159 | msg='Found dde links in output of ' + filename) | 175 | msg='Found dde links in output of ' + filename) |
| 160 | 176 | ||
| 177 | + @unittest.skipIf(*SKIP_PYPY_WIN) | ||
| 161 | def test_with_dde_utf16le(self): | 178 | def test_with_dde_utf16le(self): |
| 162 | """ check that dde links appear on stdout """ | 179 | """ check that dde links appear on stdout """ |
| 163 | filename = 'dde-test-from-office2013-utf_16le-korean.doc.zip' | 180 | filename = 'dde-test-from-office2013-utf_16le-korean.doc.zip' |
| @@ -179,6 +196,7 @@ class TestDdeLinks(unittest.TestCase): | @@ -179,6 +196,7 @@ class TestDdeLinks(unittest.TestCase): | ||
| 179 | msg='unexpected output for dde-test.{0}: {1}' | 196 | msg='unexpected output for dde-test.{0}: {1}' |
| 180 | .format(extn, output)) | 197 | .format(extn, output)) |
| 181 | 198 | ||
| 199 | + @unittest.skipIf(*SKIP_PYPY_WIN) | ||
| 182 | def test_xml(self): | 200 | def test_xml(self): |
| 183 | """ check that dde in xml from word / excel is found """ | 201 | """ check that dde in xml from word / excel is found """ |
| 184 | for filename in ('dde-in-excel2003.xml', | 202 | for filename in ('dde-in-excel2003.xml', |
tests/oleid/test_basic.py
| @@ -90,14 +90,14 @@ class TestOleIDBasic(unittest.TestCase): | @@ -90,14 +90,14 @@ class TestOleIDBasic(unittest.TestCase): | ||
| 90 | for filename, value_dict in self.oleids: | 90 | for filename, value_dict in self.oleids: |
| 91 | # print('Debugging: testing file {0}'.format(filename)) | 91 | # print('Debugging: testing file {0}'.format(filename)) |
| 92 | self.assertEqual(value_dict['ext_rels'], | 92 | self.assertEqual(value_dict['ext_rels'], |
| 93 | - '/external_link/' in filename) | 93 | + os.sep + 'external_link' + os.sep in filename) |
| 94 | 94 | ||
| 95 | def test_objectpool(self): | 95 | def test_objectpool(self): |
| 96 | """Test indicator for ObjectPool stream in ole files.""" | 96 | """Test indicator for ObjectPool stream in ole files.""" |
| 97 | for filename, value_dict in self.oleids: | 97 | for filename, value_dict in self.oleids: |
| 98 | # print('Debugging: testing file {0}'.format(filename)) | 98 | # print('Debugging: testing file {0}'.format(filename)) |
| 99 | - if (filename.startswith('oleobj/sample_with_') | ||
| 100 | - or filename.startswith('oleobj/embedded')) \ | 99 | + if (filename.startswith(join('oleobj', 'sample_with_')) |
| 100 | + or filename.startswith(join('oleobj', 'embedded'))) \ | ||
| 101 | and (filename.endswith('.doc') | 101 | and (filename.endswith('.doc') |
| 102 | or filename.endswith('.dot')): | 102 | or filename.endswith('.dot')): |
| 103 | self.assertTrue(value_dict['ObjectPool']) | 103 | self.assertTrue(value_dict['ObjectPool']) |
| @@ -106,6 +106,15 @@ class TestOleIDBasic(unittest.TestCase): | @@ -106,6 +106,15 @@ class TestOleIDBasic(unittest.TestCase): | ||
| 106 | 106 | ||
| 107 | def test_macros(self): | 107 | def test_macros(self): |
| 108 | """Test indicator for macros.""" | 108 | """Test indicator for macros.""" |
| 109 | + find_vba = ( | ||
| 110 | + join('ooxml', 'dde-in-excel2003.xml'), # not really | ||
| 111 | + join('encrypted', 'autostart-encrypt-standardpassword.xls'), | ||
| 112 | + join('msodde', 'dde-in-csv.csv'), # "Windows" "calc.exe" | ||
| 113 | + join('msodde', 'dde-in-excel2003.xml'), # same as above | ||
| 114 | + join('oleform', 'oleform-PR314.docm'), | ||
| 115 | + join('basic', 'empty'), # WTF? | ||
| 116 | + join('basic', 'text'), | ||
| 117 | + ) | ||
| 109 | for filename, value_dict in self.oleids: | 118 | for filename, value_dict in self.oleids: |
| 110 | # TODO: we need a sample file with xlm macros | 119 | # TODO: we need a sample file with xlm macros |
| 111 | before_dot, suffix = splitext(filename) | 120 | before_dot, suffix = splitext(filename) |
| @@ -119,18 +128,10 @@ class TestOleIDBasic(unittest.TestCase): | @@ -119,18 +128,10 @@ class TestOleIDBasic(unittest.TestCase): | ||
| 119 | self.assertIn(value_dict['xlm'], ('Unknown', 'No')) | 128 | self.assertIn(value_dict['xlm'], ('Unknown', 'No')) |
| 120 | 129 | ||
| 121 | # "macro detection" in text files leads to interesting results: | 130 | # "macro detection" in text files leads to interesting results: |
| 122 | - if filename in ('ooxml/dde-in-excel2003.xml', # not really | ||
| 123 | - 'encrypted/autostart-encrypt-standardpassword.xls', | ||
| 124 | - 'msodde/dde-in-csv.csv', # "Windows" "calc.exe" | ||
| 125 | - 'msodde/dde-in-excel2003.xml', # same as above | ||
| 126 | - 'oleform/oleform-PR314.docm', | ||
| 127 | - 'basic/empty', # WTF? | ||
| 128 | - 'basic/text', # no macros! | ||
| 129 | - 'olevba/sample_with_vba.ppt', | ||
| 130 | - ): | 131 | + if filename in find_vba: # no macros! |
| 131 | self.assertEqual(value_dict['vba'], 'Yes') | 132 | self.assertEqual(value_dict['vba'], 'Yes') |
| 132 | else: | 133 | else: |
| 133 | - self.assertEqual(value_dict['vba'], 'No') | 134 | + self.assertIn(value_dict['vba'], ('No', 'Error')) |
| 134 | 135 | ||
| 135 | def test_flash(self): | 136 | def test_flash(self): |
| 136 | """Test indicator for flash.""" | 137 | """Test indicator for flash.""" |
tests/oleid/test_issue_166.py
| @@ -2,17 +2,17 @@ | @@ -2,17 +2,17 @@ | ||
| 2 | Test if oleid detects encrypted documents | 2 | Test if oleid detects encrypted documents |
| 3 | """ | 3 | """ |
| 4 | 4 | ||
| 5 | -import unittest, sys, os | ||
| 6 | - | ||
| 7 | -from tests.test_utils import DATA_BASE_DIR | 5 | +import unittest |
| 8 | from os.path import join | 6 | from os.path import join |
| 7 | +from tests.test_utils import DATA_BASE_DIR | ||
| 9 | 8 | ||
| 10 | from oletools import oleid | 9 | from oletools import oleid |
| 11 | 10 | ||
| 11 | + | ||
| 12 | class TestEncryptedDocumentDetection(unittest.TestCase): | 12 | class TestEncryptedDocumentDetection(unittest.TestCase): |
| 13 | def test_encrypted_document_detection(self): | 13 | def test_encrypted_document_detection(self): |
| 14 | """ Run oleid and check if the document is flagged as encrypted """ | 14 | """ Run oleid and check if the document is flagged as encrypted """ |
| 15 | - filename = join(DATA_BASE_DIR, 'basic/encrypted.docx') | 15 | + filename = join(DATA_BASE_DIR, 'basic', 'encrypted.docx') |
| 16 | 16 | ||
| 17 | oleid_instance = oleid.OleID(filename) | 17 | oleid_instance = oleid.OleID(filename) |
| 18 | indicators = oleid_instance.check() | 18 | indicators = oleid_instance.check() |
| @@ -21,6 +21,7 @@ class TestEncryptedDocumentDetection(unittest.TestCase): | @@ -21,6 +21,7 @@ class TestEncryptedDocumentDetection(unittest.TestCase): | ||
| 21 | 21 | ||
| 22 | self.assertEqual(is_encrypted, True) | 22 | self.assertEqual(is_encrypted, True) |
| 23 | 23 | ||
| 24 | + | ||
| 24 | # just in case somebody calls this file as a script | 25 | # just in case somebody calls this file as a script |
| 25 | if __name__ == '__main__': | 26 | if __name__ == '__main__': |
| 26 | unittest.main() | 27 | unittest.main() |
| 27 | \ No newline at end of file | 28 | \ No newline at end of file |
tests/olevba/test_basic.py
| @@ -120,10 +120,14 @@ class TestOlevbaBasic(unittest.TestCase): | @@ -120,10 +120,14 @@ class TestOlevbaBasic(unittest.TestCase): | ||
| 120 | args=[full_name, ] + ADD_ARGS, | 120 | args=[full_name, ] + ADD_ARGS, |
| 121 | accept_nonzero_exit=True) | 121 | accept_nonzero_exit=True) |
| 122 | output = json.loads(out_str) | 122 | output = json.loads(out_str) |
| 123 | - self.assertEqual(len(output), 2) | 123 | + self.assertGreaterEqual(len(output), 2) |
| 124 | self.assertEqual(output[0]['type'], 'MetaInformation') | 124 | self.assertEqual(output[0]['type'], 'MetaInformation') |
| 125 | self.assertEqual(output[0]['script_name'], 'olevba') | 125 | self.assertEqual(output[0]['script_name'], 'olevba') |
| 126 | - result = output[1] | 126 | + for entry in output[1:]: |
| 127 | + if entry['type'] in ('msg', 'warning'): | ||
| 128 | + continue # ignore messages | ||
| 129 | + result = entry | ||
| 130 | + break | ||
| 127 | self.assertTrue(result['json_conversion_successful']) | 131 | self.assertTrue(result['json_conversion_successful']) |
| 128 | if suffix in ('.xlsb', '.xltm', '.xlsm'): | 132 | if suffix in ('.xlsb', '.xltm', '.xlsm'): |
| 129 | # TODO: cannot extract xlm macros for these types yet | 133 | # TODO: cannot extract xlm macros for these types yet |
tests/olevba/test_crypto.py
| @@ -40,7 +40,7 @@ class OlevbaCryptoWriteProtectTest(unittest.TestCase): | @@ -40,7 +40,7 @@ class OlevbaCryptoWriteProtectTest(unittest.TestCase): | ||
| 40 | exclude_stderr=True) | 40 | exclude_stderr=True) |
| 41 | data = json.loads(output, object_pairs_hook=OrderedDict) | 41 | data = json.loads(output, object_pairs_hook=OrderedDict) |
| 42 | # debug: json.dump(data, sys.stdout, indent=4) | 42 | # debug: json.dump(data, sys.stdout, indent=4) |
| 43 | - self.assertIn(len(data), (3, 4)) | 43 | + self.assertGreaterEqual(len(data), 3) |
| 44 | 44 | ||
| 45 | # first 2 parts: general info about script and file | 45 | # first 2 parts: general info about script and file |
| 46 | self.assertIn('script_name', data[0]) | 46 | self.assertIn('script_name', data[0]) |
| @@ -53,22 +53,23 @@ class OlevbaCryptoWriteProtectTest(unittest.TestCase): | @@ -53,22 +53,23 @@ class OlevbaCryptoWriteProtectTest(unittest.TestCase): | ||
| 53 | self.assertEqual(data[1]['type'], 'OLE') | 53 | self.assertEqual(data[1]['type'], 'OLE') |
| 54 | self.assertTrue(data[1]['json_conversion_successful']) | 54 | self.assertTrue(data[1]['json_conversion_successful']) |
| 55 | 55 | ||
| 56 | - # possible VBA stomping warning | ||
| 57 | - if len(data) == 4: | ||
| 58 | - self.assertEqual(data[2]['type'], 'msg') | ||
| 59 | - self.assertIn('VBA stomping', data[2]['msg']) | 56 | + for entry in data[2:]: |
| 57 | + if entry['type'] in ('msg', 'warning'): | ||
| 58 | + continue | ||
| 59 | + result = entry | ||
| 60 | + break | ||
| 60 | 61 | ||
| 61 | # last part is the actual result | 62 | # last part is the actual result |
| 62 | - self.assertEqual(data[-1]['container'], example_file) | ||
| 63 | - self.assertNotEqual(data[-1]['file'], example_file) | ||
| 64 | - self.assertEqual(data[-1]['type'], "OpenXML") | ||
| 65 | - analysis = data[-1]['analysis'] | 63 | + self.assertEqual(result['container'], example_file) |
| 64 | + self.assertNotEqual(result['file'], example_file) | ||
| 65 | + self.assertEqual(result['type'], "OpenXML") | ||
| 66 | + analysis = result['analysis'] | ||
| 66 | self.assertEqual(analysis[0]['type'], 'AutoExec') | 67 | self.assertEqual(analysis[0]['type'], 'AutoExec') |
| 67 | self.assertEqual(analysis[0]['keyword'], 'Auto_Open') | 68 | self.assertEqual(analysis[0]['keyword'], 'Auto_Open') |
| 68 | - macros = data[-1]['macros'] | 69 | + macros = result['macros'] |
| 69 | self.assertEqual(macros[0]['vba_filename'], 'Modul1.bas') | 70 | self.assertEqual(macros[0]['vba_filename'], 'Modul1.bas') |
| 70 | self.assertIn('Sub Auto_Open()', macros[0]['code']) | 71 | self.assertIn('Sub Auto_Open()', macros[0]['code']) |
| 71 | - self.assertTrue(data[-1]['json_conversion_successful']) | 72 | + self.assertTrue(result['json_conversion_successful']) |
| 72 | 73 | ||
| 73 | 74 | ||
| 74 | if __name__ == '__main__': | 75 | if __name__ == '__main__': |
tests/ooxml/test_zip_sub_file.py
| @@ -111,8 +111,8 @@ class TestZipSubFile(unittest.TestCase): | @@ -111,8 +111,8 @@ class TestZipSubFile(unittest.TestCase): | ||
| 111 | self.assertEqual(self.subfile.tell(), self.compare.tell()) | 111 | self.assertEqual(self.subfile.tell(), self.compare.tell()) |
| 112 | 112 | ||
| 113 | # seek backward (only implemented case: back to start) | 113 | # seek backward (only implemented case: back to start) |
| 114 | - self.subfile.seek(-self.subfile.tell(), os.SEEK_CUR) | ||
| 115 | - self.compare.seek(-self.compare.tell(), os.SEEK_CUR) | 114 | + self.subfile.seek(-1 * self.subfile.tell(), os.SEEK_CUR) |
| 115 | + self.compare.seek(-1 * self.compare.tell(), os.SEEK_CUR) | ||
| 116 | self.assertEqual(self.subfile.read(1), self.compare.read(1)) | 116 | self.assertEqual(self.subfile.read(1), self.compare.read(1)) |
| 117 | self.assertEqual(self.subfile.tell(), self.compare.tell()) | 117 | self.assertEqual(self.subfile.tell(), self.compare.tell()) |
| 118 | 118 |
tests/rtfobj/test_issue_185.py
| 1 | -import unittest, sys, os | ||
| 2 | - | 1 | +import unittest |
| 2 | +from os.path import join | ||
| 3 | from tests.test_utils import testdata_reader | 3 | from tests.test_utils import testdata_reader |
| 4 | from oletools import rtfobj | 4 | from oletools import rtfobj |
| 5 | 5 | ||
| 6 | + | ||
| 6 | class TestRtfObjIssue185(unittest.TestCase): | 7 | class TestRtfObjIssue185(unittest.TestCase): |
| 7 | def test_skip_space_after_bin_control_word(self): | 8 | def test_skip_space_after_bin_control_word(self): |
| 8 | - data = testdata_reader.read_encrypted('rtfobj/issue_185.rtf.zip') | 9 | + data = testdata_reader.read_encrypted(join('rtfobj', 'issue_185.rtf.zip')) |
| 9 | rtfp = rtfobj.RtfObjParser(data) | 10 | rtfp = rtfobj.RtfObjParser(data) |
| 10 | rtfp.parse() | 11 | rtfp.parse() |
| 11 | objects = rtfp.objects | 12 | objects = rtfp.objects |
| 12 | 13 | ||
| 13 | self.assertTrue(len(objects) == 1) | 14 | self.assertTrue(len(objects) == 1) |
| 14 | 15 | ||
| 16 | + | ||
| 15 | if __name__ == '__main__': | 17 | if __name__ == '__main__': |
| 16 | unittest.main() | 18 | unittest.main() |
tests/rtfobj/test_issue_251.py
| 1 | -import unittest, sys, os | ||
| 2 | - | 1 | +import unittest |
| 2 | +from os.path import join | ||
| 3 | from tests.test_utils import testdata_reader | 3 | from tests.test_utils import testdata_reader |
| 4 | from oletools import rtfobj | 4 | from oletools import rtfobj |
| 5 | 5 | ||
| 6 | + | ||
| 6 | class TestRtfObjIssue251(unittest.TestCase): | 7 | class TestRtfObjIssue251(unittest.TestCase): |
| 7 | def test_bin_no_param(self): | 8 | def test_bin_no_param(self): |
| 8 | - data = testdata_reader.read('rtfobj/issue_251.rtf') | 9 | + data = testdata_reader.read(join('rtfobj', 'issue_251.rtf')) |
| 9 | rtfp = rtfobj.RtfObjParser(data) | 10 | rtfp = rtfobj.RtfObjParser(data) |
| 10 | rtfp.parse() | 11 | rtfp.parse() |
| 11 | objects = rtfp.objects | 12 | objects = rtfp.objects |
| 12 | 13 | ||
| 13 | self.assertTrue(len(objects) == 1) | 14 | self.assertTrue(len(objects) == 1) |
| 14 | 15 | ||
| 16 | + | ||
| 15 | if __name__ == '__main__': | 17 | if __name__ == '__main__': |
| 16 | unittest.main() | 18 | unittest.main() |
tests/test_utils/testdata_reader.py
| @@ -100,6 +100,9 @@ def decrypt_sample(relpath): | @@ -100,6 +100,9 @@ def decrypt_sample(relpath): | ||
| 100 | 100 | ||
| 101 | Code based on test_encoding_handler.temp_file(). | 101 | Code based on test_encoding_handler.temp_file(). |
| 102 | 102 | ||
| 103 | + Note: this causes problems if running with PyPy on Windows. The `unlink` | ||
| 104 | + fails because the file is "still being used by another process". | ||
| 105 | + | ||
| 103 | :param relpath: path inside `DATA_BASE_DIR`, should end in '.zip' | 106 | :param relpath: path inside `DATA_BASE_DIR`, should end in '.zip' |
| 104 | :return: absolute path name to decrypted sample. | 107 | :return: absolute path name to decrypted sample. |
| 105 | """ | 108 | """ |