Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

backport py3 testing enhancements #239

Merged
merged 9 commits into from
Aug 18, 2020
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
- name: Run rule linter
run: python scripts/lint.py rules/

tests:
tests27:
runs-on: ubuntu-latest
needs: [code_style, rule_linter]
steps:
Expand All @@ -57,3 +57,20 @@ jobs:
- name: Run tests
run: pytest tests/

tests38:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be done with a matrix for the python version instead of duplicating the code. But as we want to get rid of Python 2 in the near future, I guess this won't have a long life anyway... 😄

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, good point. though, then the linting would happen twice?

Copy link
Member

@Ana06 Ana06 Aug 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@williballenthin

though, then the linting would happen twice?

no, I meant changing tests to the following (instead of duplicating tests into tests27 and test38:

  tests:
    name: Test in ${{ matrix.python }}
    runs-on: ubuntu-latest
    needs: [code_style, rule_linter]
    strategy:
      matrix:
        include:
          - python: 2.7
          - python: 3.8
    steps:
    - name: Checkout capa with submodules
      uses: actions/checkout@v2
      with:
        submodules: true
    - name: Set up Python ${{ matrix.python }}
      uses: actions/setup-python@v2
      with:
        python-version: ${{ matrix.python }}
    - name: Install capa
      run: pip install -e .[dev]
    - name: Run tests
      run: pytest tests/

I can send a PR as this can be useful if we want to test several python versions.

runs-on: ubuntu-latest
needs: [code_style, rule_linter]
steps:
- name: Checkout capa with submodules
uses: actions/checkout@v2
with:
submodules: true
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Install capa
run: pip install -e .[dev]
- name: Run tests
run: pytest tests/

11 changes: 4 additions & 7 deletions capa/features/freeze.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,9 @@ def dumps(extractor):
for feature, va in extractor.extract_basic_block_features(f, bb):
ret["scopes"]["basic block"].append(serialize_feature(feature) + (hex(va), (hex(f), hex(bb),)))

for insn, insnva in sorted([(insn, int(insn)) for insn in extractor.get_instructions(f, bb)]):
for insnva, insn in sorted(
[(insn.__int__(), insn) for insn in extractor.get_instructions(f, bb)], key=lambda p: p[0]
):
ret["functions"][hex(f)][hex(bb)].append(hex(insnva))

for feature, va in extractor.extract_insn_features(f, bb, insn):
Expand Down Expand Up @@ -245,12 +247,7 @@ def main(argv=None):
logging.basicConfig(level=logging.INFO)
logging.getLogger().setLevel(logging.INFO)

vw = capa.main.get_workspace(args.sample, args.format)

# don't import this at top level to support ida/py3 backend
import capa.features.extractors.viv

extractor = capa.features.extractors.viv.VivisectFeatureExtractor(vw, args.sample)
extractor = capa.main.get_extractor(args.sample, args.format)
with open(args.output, "wb") as f:
f.write(dump(extractor))

Expand Down
54 changes: 50 additions & 4 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
# See the License for the specific language governing permissions and limitations under the License.

import os
import sys
import os.path
import contextlib
import collections

import pytest
Expand All @@ -27,6 +29,44 @@
CD = os.path.dirname(__file__)


@contextlib.contextmanager
def xfail(condition, reason=None):
"""
context manager that wraps a block that is expected to fail in some cases.
when it does fail (and is expected), then mark this as pytest.xfail.
if its unexpected, raise an exception, so the test fails.

example::

# this test:
# - passes on py3 if foo() works
# - fails on py3 if foo() fails
# - xfails on py2 if foo() fails
# - fails on py2 if foo() works
with xfail(sys.version_info < (3, 0), reason="py3 doesn't foo"):
foo()
"""
try:
# do the block
yield
except:
if condition:
# we expected the test to fail, so raise and register this via pytest
pytest.xfail(reason)
else:
# we don't expect an exception, so the test should fail
raise
else:
if not condition:
# here we expect the block to run successfully,
# and we've received no exception,
# so this is good
pass
else:
# we expected an exception, but didn't find one. that's an error.
raise RuntimeError("expected to fail, but didn't")


@lru_cache()
def get_viv_extractor(path):
import capa.features.extractors.viv
Expand Down Expand Up @@ -376,14 +416,20 @@ def do_test_feature_presence(get_extractor, sample, scope, feature, expected):
def do_test_feature_count(get_extractor, sample, scope, feature, expected):
extractor = get_extractor(sample)
features = scope(extractor)
msg = "%s should be found %d times in %s" % (str(feature), expected, scope.__name__)
msg = "%s should be found %d times in %s, found: %d" % (
str(feature),
expected,
scope.__name__,
len(features[feature]),
)
assert len(features[feature]) == expected, msg


def get_extractor(path):
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we might want to merge this routine with main.get_extractor() at some point.

# decide here which extractor to load for tests.
# maybe check which python version we've loaded or if we're in IDA.
extractor = get_viv_extractor(path)
if sys.version_info >= (3, 0):
raise RuntimeError("no supported py3 backends yet")
else:
extractor = get_viv_extractor(path)

# overload the extractor so that the fixture exposes `extractor.path`
setattr(extractor, "path", path)
Expand Down
19 changes: 8 additions & 11 deletions tests/test_freeze.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.

import sys
import textwrap

import pytest
from fixtures import *

import capa.main
Expand Down Expand Up @@ -104,17 +105,14 @@ def compare_extractors_viv_null(viv_ext, null_ext):
viv_ext (capa.features.extractors.viv.VivisectFeatureExtractor)
null_ext (capa.features.extractors.NullFeatureExtractor)
"""

# TODO: ordering of these things probably doesn't work yet

assert list(viv_ext.extract_file_features()) == list(null_ext.extract_file_features())
assert to_int(list(viv_ext.get_functions())) == list(null_ext.get_functions())
assert list(map(to_int, viv_ext.get_functions())) == list(null_ext.get_functions())
for f in viv_ext.get_functions():
assert to_int(list(viv_ext.get_basic_blocks(f))) == list(null_ext.get_basic_blocks(to_int(f)))
assert list(map(to_int, viv_ext.get_basic_blocks(f))) == list(null_ext.get_basic_blocks(to_int(f)))
assert list(viv_ext.extract_function_features(f)) == list(null_ext.extract_function_features(to_int(f)))

for bb in viv_ext.get_basic_blocks(f):
assert to_int(list(viv_ext.get_instructions(f, bb))) == list(
assert list(map(to_int, viv_ext.get_instructions(f, bb))) == list(
null_ext.get_instructions(to_int(f), to_int(bb))
)
assert list(viv_ext.extract_basic_block_features(f, bb)) == list(
Expand All @@ -129,10 +127,7 @@ def compare_extractors_viv_null(viv_ext, null_ext):

def to_int(o):
"""helper to get int value of extractor items"""
if isinstance(o, list):
return map(lambda x: capa.helpers.oint(x), o)
else:
return capa.helpers.oint(o)
return capa.helpers.oint(o)


def test_freeze_s_roundtrip():
Expand Down Expand Up @@ -169,13 +164,15 @@ def test_serialize_features():
roundtrip_feature(capa.features.file.Import("#11"))


@pytest.mark.xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2")
def test_freeze_sample(tmpdir, z9324d_extractor):
# tmpdir fixture handles cleanup
o = tmpdir.mkdir("capa").join("test.frz").strpath
path = z9324d_extractor.path
assert capa.features.freeze.main([path, o, "-v"]) == 0


@pytest.mark.xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2")
def test_freeze_load_sample(tmpdir, z9324d_extractor):
o = tmpdir.mkdir("capa").join("test.frz")

Expand Down
12 changes: 10 additions & 2 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.

import sys
import textwrap

import pytest
from fixtures import *

import capa.main
import capa.rules
import capa.engine
import capa.features
import capa.features.extractors.viv
from capa.engine import *


@pytest.mark.xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2")
def test_main(z9324d_extractor):
# tests rules can be loaded successfully and all output modes
path = z9324d_extractor.path
Expand All @@ -27,6 +28,7 @@ def test_main(z9324d_extractor):
assert capa.main.main([path]) == 0


@pytest.mark.xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2")
def test_main_single_rule(z9324d_extractor, tmpdir):
# tests a single rule can be loaded successfully
RULE_CONTENT = textwrap.dedent(
Expand All @@ -45,6 +47,7 @@ def test_main_single_rule(z9324d_extractor, tmpdir):
assert capa.main.main([path, "-v", "-r", rule_file.strpath,]) == 0


@pytest.mark.xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2")
def test_main_shellcode(z499c2_extractor):
path = z499c2_extractor.path
assert capa.main.main([path, "-vv", "-f", "sc32"]) == 0
Expand Down Expand Up @@ -99,6 +102,7 @@ def test_ruleset():
assert len(rules.basic_block_rules) == 1


@pytest.mark.xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2")
def test_match_across_scopes_file_function(z9324d_extractor):
rules = capa.rules.RuleSet(
[
Expand Down Expand Up @@ -162,6 +166,7 @@ def test_match_across_scopes_file_function(z9324d_extractor):
assert ".text section and install service" in capabilities


@pytest.mark.xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2")
def test_match_across_scopes(z9324d_extractor):
rules = capa.rules.RuleSet(
[
Expand Down Expand Up @@ -224,6 +229,7 @@ def test_match_across_scopes(z9324d_extractor):
assert "kill thread program" in capabilities


@pytest.mark.xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2")
def test_subscope_bb_rules(z9324d_extractor):
rules = capa.rules.RuleSet(
[
Expand All @@ -248,6 +254,7 @@ def test_subscope_bb_rules(z9324d_extractor):
assert "test rule" in capabilities


@pytest.mark.xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2")
def test_byte_matching(z9324d_extractor):
rules = capa.rules.RuleSet(
[
Expand All @@ -270,6 +277,7 @@ def test_byte_matching(z9324d_extractor):
assert "byte match test" in capabilities


@pytest.mark.xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2")
def test_count_bb(z9324d_extractor):
rules = capa.rules.RuleSet(
[
Expand Down
7 changes: 5 additions & 2 deletions tests/test_viv_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
import sys

from fixtures import *

Expand All @@ -13,11 +14,13 @@
"sample,scope,feature,expected", FEATURE_PRESENCE_TESTS, indirect=["sample", "scope"],
)
def test_viv_features(sample, scope, feature, expected):
do_test_feature_presence(get_viv_extractor, sample, scope, feature, expected)
with xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2"):
do_test_feature_presence(get_viv_extractor, sample, scope, feature, expected)


@parametrize(
"sample,scope,feature,expected", FEATURE_COUNT_TESTS, indirect=["sample", "scope"],
)
def test_viv_feature_counts(sample, scope, feature, expected):
do_test_feature_count(get_viv_extractor, sample, scope, feature, expected)
with xfail(sys.version_info >= (3, 0), reason="vivsect only works on py2"):
do_test_feature_count(get_viv_extractor, sample, scope, feature, expected)