diff --git a/pyRCV2/ties.py b/pyRCV2/ties.py index 7453e85..fd42690 100644 --- a/pyRCV2/ties.py +++ b/pyRCV2/ties.py @@ -76,7 +76,6 @@ class TiesPrompt: def choose_highest(self, l): return self.choose_lowest(l) -# FIXME: This is untested! class TiesBackwards: """ Break ties based on the candidate who had the highest/lowest total at the end @@ -107,7 +106,6 @@ class TiesBackwards: __pragma__('noopov') return None -# FIXME: This is untested! class TiesForwards: """ Break ties based on the candidate who had the highest/lowest total at the end diff --git a/pyRCV2/transcrypt.py b/pyRCV2/transcrypt.py index 860af14..34e0471 100644 --- a/pyRCV2/transcrypt.py +++ b/pyRCV2/transcrypt.py @@ -1,5 +1,5 @@ # pyRCV2: Preferential vote counting -# Copyright © 2020 Lee Yingtong Li (RunasSudo) +# Copyright © 2020–2021 Lee Yingtong Li (RunasSudo) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -19,6 +19,7 @@ import pyRCV2.model import pyRCV2.method, pyRCV2.method.base_stv, pyRCV2.method.gregory, pyRCV2.method.meek import pyRCV2.numbers import pyRCV2.random +import pyRCV2.safedict import pyRCV2.ties __pragma__('js', '{}', 'export {pyRCV2, isinstance, repr, str};') diff --git a/tests/test_combinations.py b/tests/test_combinations.py index 5b92d6a..17c429f 100644 --- a/tests/test_combinations.py +++ b/tests/test_combinations.py @@ -18,9 +18,9 @@ import pyRCV2.blt import pyRCV2.numbers import pyRCV2.method.gregory, pyRCV2.method.meek from pyRCV2.model import CountCompleted +import tests.util import json -from py_mini_racer import py_mini_racer def maketst(numbers, counter_cls, options, options_description): def t_py(): @@ -41,17 +41,7 @@ def maketst(numbers, counter_cls, options, options_description): # TODO: Do some sanity checks def t_js(): - ctx = py_mini_racer.MiniRacer() - - # Imports - with open('html/vendor/BigInt_BigRat-a5f89e2.min.js', 'r') as f: - ctx.eval(f.read()) - with open('html/vendor/big-6.0.0.min.js', 'r') as f: - ctx.eval(f.read()) - with open('html/vendor/sjcl-1.0.8.min.js', 'r') as f: - ctx.eval(f.read()) - with open('html/bundle.js', 'r') as f: - ctx.eval(f.read()) + ctx = tests.util.init_context() ctx.eval('py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.Fixed);') ctx.eval('py.pyRCV2.numbers.set_dps(5);') diff --git a/tests/test_numbers.py b/tests/test_numbers.py index d74ab06..b76b6c4 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -16,8 +16,7 @@ import pyRCV2.numbers from pyRCV2.numbers import Num - -from py_mini_racer import py_mini_racer +import tests.util def maketst(numbers, dps, method, result): def t_py(): @@ -30,23 +29,13 @@ def maketst(numbers, dps, method, result): assert getattr(num1, method)(num2) == Num(result) def t_js(): - ctx = py_mini_racer.MiniRacer() - - # Imports - with open('html/vendor/BigInt_BigRat-a5f89e2.min.js', 'r') as f: - ctx.eval(f.read()) - with open('html/vendor/big-6.0.0.min.js', 'r') as f: - ctx.eval(f.read()) - with open('html/vendor/sjcl-1.0.8.min.js', 'r') as f: - ctx.eval(f.read()) - with open('html/bundle.js', 'r') as f: - ctx.eval(f.read()) + ctx = tests.util.init_context() ctx.eval('py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.{});'.format(numbers)) ctx.eval('py.pyRCV2.numbers.set_dps({});'.format(dps)) - ctx.eval('num1 = py.pyRCV2.numbers.Num("314.15"); void(0);') - ctx.eval('num2 = py.pyRCV2.numbers.Num("42.42"); void(0);') + ctx.eval('let num1 = py.pyRCV2.numbers.Num("314.15");') + ctx.eval('let num2 = py.pyRCV2.numbers.Num("42.42");') assert ctx.eval('num1.{}(num2).__eq__(py.pyRCV2.numbers.Num("{}"))'.format(method, result)) @@ -66,22 +55,12 @@ def maketst_round(numbers, dps, num, dps_round, mode_round, result): assert num1.round(dps_round, getattr(num1, mode_round)) == Num(result) def t_js(): - ctx = py_mini_racer.MiniRacer() - - # Imports - with open('html/vendor/BigInt_BigRat-a5f89e2.min.js', 'r') as f: - ctx.eval(f.read()) - with open('html/vendor/big-6.0.0.min.js', 'r') as f: - ctx.eval(f.read()) - with open('html/vendor/sjcl-1.0.8.min.js', 'r') as f: - ctx.eval(f.read()) - with open('html/bundle.js', 'r') as f: - ctx.eval(f.read()) + ctx = tests.util.init_context() ctx.eval('py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.{});'.format(numbers)) ctx.eval('py.pyRCV2.numbers.set_dps({});'.format(dps)) - ctx.eval('num1 = py.pyRCV2.numbers.Num("{}"); void(0);'.format(num)) + ctx.eval('let num1 = py.pyRCV2.numbers.Num("{}");'.format(num)) assert ctx.eval('num1.round({}, num1.{}).__eq__(py.pyRCV2.numbers.Num("{}"))'.format(dps_round, mode_round, result)) return t_py, t_js diff --git a/tests/test_random.py b/tests/test_random.py index d387e84..09e5276 100644 --- a/tests/test_random.py +++ b/tests/test_random.py @@ -15,8 +15,7 @@ # along with this program. If not, see . from pyRCV2.random import SHARandom - -from py_mini_racer import py_mini_racer +import tests.util def test_sharandom_py(): random = SHARandom('foobar') @@ -25,17 +24,7 @@ def test_sharandom_py(): assert random.next(64) == 13 def test_sharandom_js(): - ctx = py_mini_racer.MiniRacer() - - # Imports - with open('html/vendor/BigInt_BigRat-a5f89e2.min.js', 'r') as f: - ctx.eval(f.read()) - with open('html/vendor/big-6.0.0.min.js', 'r') as f: - ctx.eval(f.read()) - with open('html/vendor/sjcl-1.0.8.min.js', 'r') as f: - ctx.eval(f.read()) - with open('html/bundle.js', 'r') as f: - ctx.eval(f.read()) + ctx = tests.util.init_context() ctx.eval('let random = py.pyRCV2.random.SHARandom("foobar");') assert ctx.eval('random.py_next(10)') == 0 diff --git a/tests/test_ties.py b/tests/test_ties.py new file mode 100644 index 0000000..0ab89d0 --- /dev/null +++ b/tests/test_ties.py @@ -0,0 +1,145 @@ +# pyRCV2: Preferential vote counting +# Copyright © 2020–2021 Lee Yingtong Li (RunasSudo) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import pytest + +from pyRCV2.model import Candidate, CountCard +from pyRCV2.numbers import Num +from pyRCV2.ties import TiesBackwards, TiesForwards, TiesPrompt, TiesRandom +import tests.util + +def test_prompt_py(monkeypatch): + monkeypatch.setattr('builtins.input', lambda _: '2') + l = [ + (Candidate('A'), CountCard()), + (Candidate('B'), CountCard()), + (Candidate('C'), CountCard()), + ] + t = TiesPrompt() + assert t.choose_lowest(l) == l[1] + assert t.choose_highest(l) == l[1] + +def test_prompt_js(): + ctx = tests.util.init_context() + ctx.eval('let l = [[py.pyRCV2.model.Candidate("A"), py.pyRCV2.model.CountCard()], [py.pyRCV2.model.Candidate("B"), py.pyRCV2.model.CountCard()], [py.pyRCV2.model.Candidate("C"), py.pyRCV2.model.CountCard()]];') + ctx.eval('let tie = py.pyRCV2.ties.TiesPrompt();') + ctx.eval('let raised = false; try { tie.choose_lowest(l); } catch (ex) { if (py.isinstance(ex, py.pyRCV2.ties.RequireInput)) { raised = true; } }') + assert ctx.eval('raised') == True + + ctx.eval('tie.buffer = "2";') + assert ctx.eval('tie.choose_lowest(l) === l[1]') + ctx.eval('tie.buffer = "2";') + assert ctx.eval('tie.choose_highest(l) === l[1]') + +class Stub: + pass + +def test_backwards_py(): + l = [ + (Candidate('A'), CountCard()), + (Candidate('B'), CountCard()), + (Candidate('C'), CountCard()), + ] + + # Prepare previous results + result1 = Stub() + result1.candidates = {l[0][0]: CountCard(), l[1][0]: CountCard(), l[2][0]: CountCard()} + result1.candidates[l[0][0]].orig_votes = Num(10) + result1.candidates[l[1][0]].orig_votes = Num(20) + result1.candidates[l[2][0]].orig_votes = Num(30) + + result2 = Stub() + result2.candidates = {l[0][0]: CountCard(), l[1][0]: CountCard(), l[2][0]: CountCard()} + result2.candidates[l[0][0]].orig_votes = Num(30) + result2.candidates[l[1][0]].orig_votes = Num(20) + result2.candidates[l[2][0]].orig_votes = Num(10) + + counter = Stub() + counter.step_results = [result1, result2] + + t = TiesBackwards(counter) + assert t.choose_lowest(l) == l[2] + assert t.choose_highest(l) == l[0] + +def test_backwards_js(): + ctx = tests.util.init_context() + ctx.eval('let l = [[py.pyRCV2.model.Candidate("A"), py.pyRCV2.model.CountCard()], [py.pyRCV2.model.Candidate("B"), py.pyRCV2.model.CountCard()], [py.pyRCV2.model.Candidate("C"), py.pyRCV2.model.CountCard()]];') + + # Prepare previous results + ctx.eval('let result1 = {candidates: py.pyRCV2.safedict.SafeDict([[l[0][0], {votes: py.pyRCV2.numbers.Num(10)}], [l[1][0], {votes: py.pyRCV2.numbers.Num(20)}], [l[2][0], {votes: py.pyRCV2.numbers.Num(30)}]])};') + ctx.eval('let result2 = {candidates: py.pyRCV2.safedict.SafeDict([[l[0][0], {votes: py.pyRCV2.numbers.Num(30)}], [l[1][0], {votes: py.pyRCV2.numbers.Num(20)}], [l[2][0], {votes: py.pyRCV2.numbers.Num(10)}]])};') + ctx.eval('let counter = {step_results: [result1, result2]};') + + ctx.eval('let tie = py.pyRCV2.ties.TiesBackwards(counter);') + assert ctx.eval('tie.choose_lowest(l) === l[2]') + assert ctx.eval('tie.choose_highest(l) === l[0]') + +def test_forwards_py(): + l = [ + (Candidate('A'), CountCard()), + (Candidate('B'), CountCard()), + (Candidate('C'), CountCard()), + ] + + # Prepare previous results + result1 = Stub() + result1.candidates = {l[0][0]: CountCard(), l[1][0]: CountCard(), l[2][0]: CountCard()} + result1.candidates[l[0][0]].orig_votes = Num(10) + result1.candidates[l[1][0]].orig_votes = Num(20) + result1.candidates[l[2][0]].orig_votes = Num(30) + + result2 = Stub() + result2.candidates = {l[0][0]: CountCard(), l[1][0]: CountCard(), l[2][0]: CountCard()} + result2.candidates[l[0][0]].orig_votes = Num(30) + result2.candidates[l[1][0]].orig_votes = Num(20) + result2.candidates[l[2][0]].orig_votes = Num(10) + + counter = Stub() + counter.step_results = [result1, result2] + + t = TiesForwards(counter) + assert t.choose_lowest(l) == l[0] + assert t.choose_highest(l) == l[2] + +def test_forwards_js(): + ctx = tests.util.init_context() + ctx.eval('let l = [[py.pyRCV2.model.Candidate("A"), py.pyRCV2.model.CountCard()], [py.pyRCV2.model.Candidate("B"), py.pyRCV2.model.CountCard()], [py.pyRCV2.model.Candidate("C"), py.pyRCV2.model.CountCard()]];') + + # Prepare previous results + ctx.eval('let result1 = {candidates: py.pyRCV2.safedict.SafeDict([[l[0][0], {votes: py.pyRCV2.numbers.Num(10)}], [l[1][0], {votes: py.pyRCV2.numbers.Num(20)}], [l[2][0], {votes: py.pyRCV2.numbers.Num(30)}]])};') + ctx.eval('let result2 = {candidates: py.pyRCV2.safedict.SafeDict([[l[0][0], {votes: py.pyRCV2.numbers.Num(30)}], [l[1][0], {votes: py.pyRCV2.numbers.Num(20)}], [l[2][0], {votes: py.pyRCV2.numbers.Num(10)}]])};') + ctx.eval('let counter = {step_results: [result1, result2]};') + + ctx.eval('let tie = py.pyRCV2.ties.TiesForwards(counter);') + assert ctx.eval('tie.choose_lowest(l) === l[0]') + assert ctx.eval('tie.choose_highest(l) === l[2]') + +def test_random_py(): + l = [ + (Candidate('A'), CountCard()), + (Candidate('B'), CountCard()), + (Candidate('C'), CountCard()), + ] + t = TiesRandom('foobar') # first number is 0, second number is 0 + assert t.choose_lowest(l) == l[0] + assert t.choose_highest(l) == l[0] + +def test_random_js(): + ctx = tests.util.init_context() + ctx.eval('let l = [[py.pyRCV2.model.Candidate("A"), py.pyRCV2.model.CountCard()], [py.pyRCV2.model.Candidate("B"), py.pyRCV2.model.CountCard()], [py.pyRCV2.model.Candidate("C"), py.pyRCV2.model.CountCard()]];') + ctx.eval('let tie = py.pyRCV2.ties.TiesRandom("foobar")') + assert ctx.eval('tie.choose_lowest(l) === l[0]') + assert ctx.eval('tie.choose_highest(l) === l[0]') diff --git a/tests/util.py b/tests/util.py new file mode 100644 index 0000000..9f4fae3 --- /dev/null +++ b/tests/util.py @@ -0,0 +1,32 @@ +# pyRCV2: Preferential vote counting +# Copyright © 2020–2021 Lee Yingtong Li (RunasSudo) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from py_mini_racer import py_mini_racer + +def init_context(): + ctx = py_mini_racer.MiniRacer() + + # Imports + with open('html/vendor/BigInt_BigRat-a5f89e2.min.js', 'r') as f: + ctx.eval(f.read()) + with open('html/vendor/big-6.0.0.min.js', 'r') as f: + ctx.eval(f.read()) + with open('html/vendor/sjcl-1.0.8.min.js', 'r') as f: + ctx.eval(f.read()) + with open('html/bundle.js', 'r') as f: + ctx.eval(f.read()) + + return ctx