Skip to content

Commit

Permalink
Switch from Travis CI to GitHub Actions
Browse files Browse the repository at this point in the history
Part of this migration involved refactoring the test pipeline to use
nose2 instead of nose.
  • Loading branch information
caleb531 committed Oct 5, 2022
1 parent 493db05 commit d8bc0cd
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 90 deletions.
44 changes: 44 additions & 0 deletions .github/workflows/tests.yml
@@ -0,0 +1,44 @@
# This workflow will install Python dependencies and run tests with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions

name: tests

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
test:

runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10"]

steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Test with nose2
run: |
coverage run -m nose2 tests
coverage lcov -o cover/coverage.lcov
- name: Coveralls
uses: coverallsapp/github-action@master
with:
path-to-lcov: cover/coverage.lcov
github-token: ${{ secrets.GITHUB_TOKEN }}
2 changes: 1 addition & 1 deletion .python-version
@@ -1 +1 @@
3.5
3.10
16 changes: 0 additions & 16 deletions .travis.yml

This file was deleted.

2 changes: 1 addition & 1 deletion LICENSE.txt
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2015-2018 Caleb Evans
Copyright (c) 2015-2022 Caleb Evans

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
6 changes: 3 additions & 3 deletions README.md
@@ -1,10 +1,10 @@
# Three of a Crime Solver

*Copyright 2015-2018 Caleb Evans*
*Copyright 2015-2022 Caleb Evans*
*Released under the MIT license*

[![Build Status](https://app.travis-ci.com/caleb531/three-of-a-crime.svg?branch=master)](https://app.travis-ci.com/caleb531/three-of-a-crime)
[![Coverage Status](https://coveralls.io/repos/caleb531/three-of-a-crime/badge.svg?branch=master)](https://coveralls.io/r/caleb531/three-of-a-crime?branch=master)
[![tests](https://github.com/caleb531/three-of-a-crime/actions/workflows/tests.yml/badge.svg)](https://github.com/caleb531/three-of-a-crime/actions/workflows/tests.yml)
[![Coverage Status](https://coveralls.io/repos/caleb531/three-of-a-crime/badge.svg?branch=main)](https://coveralls.io/r/caleb531/three-of-a-crime?branch=main)

This project was created and named for the Gameright game, [Three of a Crime](http://www.gamewright.com/gamewright/index.php?page=game&section=games&show=214). To understand the purpose of this project, one must first understand how the game works.

Expand Down
21 changes: 6 additions & 15 deletions requirements.txt
@@ -1,15 +1,6 @@
colorama==0.3.9
coverage==5.3
flake8==3.6.0
flake8-polyfill==1.0.2
mando==0.6.4
mccabe==0.6.1
mock==2.0.0
nose==1.3.7
pbr==3.1.1
pycodestyle==2.4.0
pyflakes==2.0.0
radon==2.2.0
rednose==1.3.0
six==1.11.0
termstyle==0.1.11
coverage==6.5.0
flake8==5.0.4
mccabe==0.7.0
nose2==0.12.0
pycodestyle==2.9.1
pyflakes==2.5.0
85 changes: 47 additions & 38 deletions tests/test_dealer.py
@@ -1,34 +1,37 @@
#!/usr/bin/env python3

import contextlib
import copy
import io
import json
import subprocess
import sys
import nose.tools as nose
import toac.dealer as dealer
from mock import ANY, Mock, NonCallableMagicMock, patch
import unittest
from contextlib import redirect_stdout
from io import StringIO
from unittest.mock import ANY, Mock, NonCallableMagicMock, patch


case = unittest.TestCase()


def test_create_game():
"""should create game object with correct properties"""
nose.assert_dict_equal(
case.assertDictEqual(
dealer.create_game(3), {'id': 3, 'winner': None, 'rounds': 0})


def test_create_deck():
"""should create shuffled deck"""
deck = dealer.create_deck()
nose.assert_is_instance(deck, list)
nose.assert_not_equal(deck, dealer.BASE_DECK)
nose.assert_set_equal(set(deck), set(dealer.BASE_DECK))
case.assertIsInstance(deck, list)
case.assertNotEqual(deck, dealer.BASE_DECK)
case.assertSetEqual(set(deck), set(dealer.BASE_DECK))


def test_build_data_object():
"""should create correct data object to pass to player"""
data = dealer.build_data_object()
nose.assert_equal(data, {
case.assertEqual(data, {
'base_suspects': list(dealer.BASE_SUSPECTS),
'match_length': 3,
'cards': [],
Expand All @@ -40,7 +43,7 @@ def test_get_match_count():
"""should calculate correct number of suspects shared by two cards"""
real_suspects = {'hbu', 'pto', 'lel'}
suspects = {'lel', 'pto', 'nnn'}
nose.assert_equal(dealer.get_match_count(suspects, real_suspects), 2)
case.assertEqual(dealer.get_match_count(suspects, real_suspects), 2)


@patch('subprocess.Popen', return_value=Mock(
Expand All @@ -55,7 +58,7 @@ def test_get_player_guess(popen):
player['program'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
popen.return_value.communicate.assert_called_once_with(
input=json.dumps(data, separators=(',', ':')).encode('utf-8'))
nose.assert_set_equal(
case.assertSetEqual(
guessed_suspects, {'hbu', 'lel', 'pto'})


Expand All @@ -64,14 +67,15 @@ def test_add_card_to_data():
suspects = {'lel', 'pto', 'nnn'}
match_count = 2
dealer.add_card_to_data(data, suspects, match_count)
nose.assert_equal(len(data['cards']), 1)
nose.assert_list_equal(data['cards'], [{
case.assertEqual(len(data['cards']), 1)
case.assertListEqual(data['cards'], [{
'suspects': tuple(suspects),
'match_count': match_count
}])


def test_print_game_stats():
@patch('toac.dealer.print')
def test_print_game_stats(print):
"""should print statistics for each game"""
game = {'id': 1, 'winner': 2, 'rounds': 3}
lock = NonCallableMagicMock()
Expand Down Expand Up @@ -104,7 +108,7 @@ class TestRunGame(object):
{"pto", "lel", "nnn"}, {"pto", "hbu", "lel"}
]

def setup(self):
def setUp(self):
self.lock = NonCallableMagicMock()

@patch('toac.dealer.create_game', return_value=copy.deepcopy(GAME))
Expand All @@ -116,11 +120,12 @@ def test_run_game(self, get_player_guess, build_data_object, create_deck,
"""should run game with given players, taking turns as necessary"""
game = create_game.return_value
data = build_data_object.return_value
dealer.run_game(1, self.PLAYERS, self.lock)
nose.assert_equal(game['winner'], 1)
nose.assert_equal(game['rounds'], 4)
nose.assert_equal(len(data['cards']), 4)
nose.assert_equal(len(data['previous_guesses']), 3)
with redirect_stdout(StringIO()):
dealer.run_game(1, self.PLAYERS, self.lock)
case.assertEqual(game['winner'], 1)
case.assertEqual(game['rounds'], 4)
case.assertEqual(len(data['cards']), 4)
case.assertEqual(len(data['previous_guesses']), 3)

@patch('toac.dealer.create_game', return_value=copy.deepcopy(GAME))
@patch('toac.dealer.create_deck', return_value=copy.deepcopy(DECK))
Expand All @@ -131,11 +136,12 @@ def test_exhaust_deck(self, get_player_guess, build_data_object,
"""should fail gracefully if deck is exhausted during gameplay"""
game = create_game.return_value
data = build_data_object.return_value
dealer.run_game(1, self.PLAYERS, self.lock)
nose.assert_equal(game['winner'], None)
nose.assert_equal(game['rounds'], 4)
nose.assert_equal(len(data['cards']), 4)
nose.assert_equal(len(data['previous_guesses']), 3)
with redirect_stdout(StringIO()):
dealer.run_game(1, self.PLAYERS, self.lock)
case.assertEqual(game['winner'], None)
case.assertEqual(game['rounds'], 4)
case.assertEqual(len(data['cards']), 4)
case.assertEqual(len(data['previous_guesses']), 3)

@patch('toac.dealer.create_game', return_value=copy.deepcopy(GAME))
@patch('toac.dealer.create_deck', return_value=copy.deepcopy(DECK))
Expand All @@ -146,19 +152,20 @@ def test_invalid_json(self, get_player_guess, build_data_object,
"""should silently fail when invalid JSON produces ValueError"""
game = create_game.return_value
data = build_data_object.return_value
dealer.run_game(1, self.PLAYERS, self.lock)
nose.assert_equal(game['winner'], None)
nose.assert_equal(game['rounds'], 1)
nose.assert_equal(len(data['cards']), 1)
nose.assert_equal(len(data['previous_guesses']), 0)
with redirect_stdout(StringIO()):
dealer.run_game(1, self.PLAYERS, self.lock)
case.assertEqual(game['winner'], None)
case.assertEqual(game['rounds'], 1)
case.assertEqual(len(data['cards']), 1)
case.assertEqual(len(data['previous_guesses']), 0)


def test_get_finished_games():
"""should yield game when joining respective process"""
processes = [Mock(), Mock(), Mock()]
games = dealer.get_finished_games(processes)
for game, process in zip(games, processes):
nose.assert_equal(process.get.call_count, 1)
case.assertEqual(process.get.call_count, 1)


def test_get_sorted_player_wins():
Expand All @@ -172,7 +179,7 @@ def test_get_sorted_player_wins():
{'id': 6, 'rounds': 3, 'winner': 'P3'}
]
all_wins = list(dealer.get_sorted_player_wins(games))
nose.assert_list_equal(all_wins, [
case.assertListEqual(all_wins, [
('P2', 3), ('P3', 2), ('P1', 1)
])

Expand All @@ -188,7 +195,7 @@ def test_print_player_wins(print):
{'id': 5, 'winner': None, 'rounds': 34}
]
dealer.print_player_wins(games)
nose.assert_equal(print.call_count, 3)
case.assertEqual(print.call_count, 3)
print.assert_any_call('P3 Wins: 2')
print.assert_any_call('P2 Wins: 1')
print.assert_any_call('P1 Wins: 1')
Expand All @@ -199,19 +206,20 @@ def test_run_games_mock_pool(apply_async):
"""should run every game asynchronously in separate (mocked) pool"""
players = [{'id': 1, 'wins': 0, 'program': './toac/player.py'}]
num_games = 5
dealer.run_games(num_games, players)
nose.assert_equal(apply_async.call_count, num_games)
with redirect_stdout(StringIO()):
dealer.run_games(num_games, players)
case.assertEqual(apply_async.call_count, num_games)
apply_async.assert_any_call(
dealer.run_game, args=(1, players, ANY))
nose.assert_equal(apply_async.return_value.get.call_count, 5)
case.assertEqual(apply_async.return_value.get.call_count, 5)


def test_create_players():
"""should create list of player objects from list of program paths"""
programs = ['./p1', './p2', './p3']
players = dealer.create_players(programs)
for p, (program, player) in enumerate(zip(programs, players)):
nose.assert_dict_equal(player, {
case.assertDictEqual(player, {
'program': program, 'wins': 0, 'id': 'P{}'.format(p + 1)
})

Expand All @@ -222,5 +230,6 @@ def test_main(run_games):
"""should run dealer program when executed directly"""
programs = sys.argv[2:]
players = dealer.create_players(programs)
dealer.main()
with redirect_stdout(StringIO()):
dealer.main()
run_games.assert_called_once_with(10, players)

0 comments on commit d8bc0cd

Please sign in to comment.