From 734953fdb34c0ca7ed804a660a385c619faabc7c Mon Sep 17 00:00:00 2001
From: Jacob Repp <jacobrepp@gmail.com>
Date: Sat, 25 Feb 2017 07:09:31 -0800
Subject: [PATCH] Pushing latest fixes and features to dev (#46)

* Adding 2017 to LICENSE

* Adding inital test suite

* Fix incorrect header argument

* Adding game metadata support (MMR, etc)
---
 .gitignore                      |  1 +
 LICENSE                         |  2 +-
 s2protocol/__init__.py          |  1 +
 s2protocol/s2_cli.py            | 30 +++++----------
 s2protocol/versions/__init__.py | 19 +++++----
 tests/suite.py                  | 68 +++++++++++++++++++++++++++++++++
 tests/test_versions.py          | 18 +++++++++
 7 files changed, 109 insertions(+), 30 deletions(-)
 create mode 100644 tests/suite.py
 create mode 100644 tests/test_versions.py

diff --git a/.gitignore b/.gitignore
index 5e5ba13..7879315 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
 
 *.swp
 *.swo
+*~
 
 *.SC2Replay
 
diff --git a/LICENSE b/LICENSE
index 08ffbea..eaa445c 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2013 Blizzard Entertainment
+Copyright (c) 2013, 2017 Blizzard Entertainment
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/s2protocol/__init__.py b/s2protocol/__init__.py
index 54f0b87..dad74cc 100644
--- a/s2protocol/__init__.py
+++ b/s2protocol/__init__.py
@@ -1,2 +1,3 @@
 __author__ = 'Blizzard Entertainment'
 __version__ = (1, 0, 1, 'dev')
+__all__ = [ 'versions', 'diff', 's2_cli' ]
diff --git a/s2protocol/s2_cli.py b/s2protocol/s2_cli.py
index 0deb12d..4b4b65a 100755
--- a/s2protocol/s2_cli.py
+++ b/s2protocol/s2_cli.py
@@ -1,24 +1,4 @@
 #!/usr/bin/env python
-#
-# Copyright (c) 2013 Blizzard Entertainment
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
 
 import sys
 import argparse
@@ -52,6 +32,7 @@ def process(self, event):
         print >> self._output, json.dumps(event, encoding='ISO-8859-1', ensure_ascii=True, indent=4)
         return event
 
+
 class NDJSONOutputFilter(EventFilter):
     """ Added as a filter will format the event into NDJSON """
     def __init__(self, output):
@@ -171,6 +152,8 @@ def main():
                         action="store_true")
     parser.add_argument("--header", help="print protocol header",
                         action="store_true")
+    parser.add_argument("--metadata", help="print game metadata",
+                        action="store_true")
     parser.add_argument("--details", help="print protocol details",
                         action="store_true")
     parser.add_argument("--initdata", help="print protocol initdata",
@@ -249,7 +232,7 @@ def process_event(event):
     contents = archive.header['user_data_header']['content']
     header = versions.latest().decode_replay_header(contents)
     if args.header:
-        process_event(args.header)
+        process_event(header)
 
     # The header's baseBuild determines which protocol to use
     baseBuild = header['m_version']['m_baseBuild']
@@ -259,6 +242,11 @@ def process_event(event):
         print >> sys.stderr, 'Unsupported base build: %d' % baseBuild
         sys.exit(1)
 
+    # Process game metadata
+    if args.all or args.metadata:
+        contents = archive.read_file('replay.gamemetadata.json')
+        process_event(json.loads(contents))
+
     # Print protocol details
     if args.all or args.details:
         contents = archive.read_file('replay.details')
diff --git a/s2protocol/versions/__init__.py b/s2protocol/versions/__init__.py
index 120f23c..385d093 100644
--- a/s2protocol/versions/__init__.py
+++ b/s2protocol/versions/__init__.py
@@ -1,26 +1,29 @@
+
 import os
 import re
 import imp
 import sys
 
 
-def import_protocol(base_path, module_name):
+def _import_protocol(base_path, protocol_module_name):
     """
     Import a module from a base path, used to import protocol modules.
     """
 
     # Try to return the module if it's been loaded already
     try:
-        return sys.modules[module_name]
+        return sys.modules[protocol_module_name]
     except KeyError:
         pass
 
-    # print 'importing', module_name, 'from', base_path
     # If any of the following calls raises an exception,
     # there's a problem we can't handle -- let the caller handle it.
-    fp, pathname, description = imp.find_module(module_name, [base_path])
+    #
+    # Without the full module name in the load, the 'import decoders' will fail
+    #
+    fp, pathname, description = imp.find_module(protocol_module_name, [base_path])
     try:
-        return imp.load_module(module_name, fp, pathname, description)
+        return imp.load_module('s2protocol.versions.' + protocol_module_name, fp, pathname, description)
     finally:
         # Since we may exit via an exception, close fp explicitly.
         if fp:
@@ -29,7 +32,7 @@ def import_protocol(base_path, module_name):
 
 def list_all(base_path=None):
     """
-    Returns a list of the current protocol version file names in the versions module.
+    Returns a list of the current protocol version file names in the versions module sorted by name.
     """
     if base_path is None:
         base_path = os.path.dirname(__file__)
@@ -58,7 +61,7 @@ def latest():
     module_name = latest_version.split('.')[0]
     
     # Perform the import
-    return import_protocol(base_path, module_name)
+    return _import_protocol(base_path, module_name)
 
 
 
@@ -67,5 +70,5 @@ def build(build_version):
     Get the module for a specific build version
     """
     base_path = os.path.dirname(__file__)
-    return import_protocol(base_path, 'protocol{0}'.format(build_version))
+    return _import_protocol(base_path, 'protocol{0}'.format(build_version))
 
diff --git a/tests/suite.py b/tests/suite.py
new file mode 100644
index 0000000..c887b83
--- /dev/null
+++ b/tests/suite.py
@@ -0,0 +1,68 @@
+#!/usr/bin/python
+
+import sys
+import unittest
+import os
+import inspect
+import re
+
+from optparse import OptionParser
+
+#
+# Fix up import path if running directly
+#
+if __name__ == '__main__':
+    filename = os.path.abspath(inspect.getfile(inspect.currentframe()))
+    thispath = os.path.dirname(filename)
+    normpath = os.path.normpath(os.path.join(thispath, os.pardir))
+    sys.path.insert(0, normpath)
+
+import s2protocol
+import test_versions
+
+
+def run():
+    parser = OptionParser()
+    parser.add_option('-l', '--list', dest='list', action='store_true',
+        help='List all test cases')
+    parser.add_option('-r', '--requests', dest='requests', action='store_true',
+        help='Enable logging of requests')
+    parser.add_option('-v', '--verbose', dest='verbose', action='store_true',
+        help='Enable verbose logging')
+    parser.add_option('-f', '--filter', dest='filter', type='string', action='store',
+        help='Filter test cases with a regular expression')
+    options, args = parser.parse_args()
+
+    all_tests = [
+        test_versions.suite(),
+    ]
+
+    if options.list:
+        for suite in all_tests:
+            for t in suite:
+                print t.id()
+        return
+
+    if options.filter:
+        pattern = re.compile(options.filter)
+        def expand_tests():
+            for suite in all_tests:
+                for t in suite:
+                    yield t
+
+        def pattern_match(test):
+            name = test.id()
+            return pattern.match(name) is not None
+        all_tests = unittest.TestSuite(filter(pattern_match, expand_tests()))
+    else:
+        all_tests = unittest.TestSuite(all_tests)
+
+    test_verbosity = 1
+    if options.verbose:
+        test_verbosity = 3
+
+    unittest.TextTestRunner(verbosity=test_verbosity, failfast=True).run(all_tests)
+
+if __name__ == '__main__':
+    run()
+
diff --git a/tests/test_versions.py b/tests/test_versions.py
new file mode 100644
index 0000000..8c42770
--- /dev/null
+++ b/tests/test_versions.py
@@ -0,0 +1,18 @@
+import unittest
+from s2protocol import versions as _versions
+
+class VersionsTestCase(unittest.TestCase):
+    def test_latest(self):
+        p = _versions.latest()
+        self.assertIsNotNone(p)
+
+    def test_specific(self):
+        p = _versions.build(49716)
+        self.assertIsNotNone(p)
+
+    def test_missing(self):
+        self.assertRaises(ImportError, lambda: _versions.build(42))
+
+
+def suite():
+    return unittest.TestLoader().loadTestsFromTestCase(VersionsTestCase)