Skip to content

Commit

Permalink
more tests and small corrections
Browse files Browse the repository at this point in the history
  • Loading branch information
traducha committed May 13, 2022
1 parent d15bd13 commit ab4ea1e
Show file tree
Hide file tree
Showing 9 changed files with 572 additions and 15 deletions.
10 changes: 7 additions & 3 deletions configuration/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class Config:
The rest of attributes are simply arguments defined in parser.py and described there
"""
# parsed arguments that have default value defined in parser.py
# each one should be defined here (as None), so the IDE knows the class has these attributes
n = None
q = None
avg_deg = None
Expand Down Expand Up @@ -62,6 +63,8 @@ class Config:
num_parties = None

config_file = None
# alternative electoral systems can be passed only in the configuration file
alternative_systems = None

# derivatives of parsed arguments and others
initialize_states = staticmethod(ng.default_initial_state)
Expand All @@ -87,9 +90,6 @@ class Config:
not_zealot_state = None
all_states = None # the order matters in the mutation function! zealot first

# alternative electoral systems can be passed only in the configuration file
alternative_systems = None

@staticmethod
def wrap_configuration(voting_function, **kwargs):
"""
Expand Down Expand Up @@ -248,6 +248,9 @@ def f(n, g):

if self.mass_media is None:
self.mass_media = 1.0 / self.num_parties # the symmetric case with no propaganda
elif self.mass_media < 0 or self.mass_media > 1:
raise ValueError(f'The mass_media parameter should be in the range [0,1], '
f'mass_media={self.mass_media} was provided.')

# Initialization in the consensus state
if self.consensus:
Expand Down Expand Up @@ -338,6 +341,7 @@ def f(n, g):
alt['total_seats'] = alt['seats'][0]
else:
alt['total_seats'] = self.total_seats
alt['seats'] = None

self.voting_systems[alt['name']] = self.wrap_configuration(
es.single_district_voting, states=self.all_states, total_seats=alt['total_seats'],
Expand Down
Empty file added configuration/tests/__init__.py
Empty file.
333 changes: 333 additions & 0 deletions configuration/tests/configuration_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
# -*- coding: utf-8 -*-
import unittest
import json
from argparse import Namespace

from configuration.config import Config, num_to_chars, generate_state_labels
from configuration.parser import parser
from electoral_sys.seat_assignment import seat_assignment_rules
from simulation.base import majority_propagation
from net_generation.base import consensus_initial_state


class DummyParser(Namespace):

def __init__(self, **kwargs):
self.alternative_systems = None
self.avg_deg = 12.0
self.config_file = None
self.consensus = False
self.district_coords = None
self.district_sizes = None
self.epsilon = 0.01
self.euclidean = False
self.mass_media = None
self.mc_steps = 50
self.n = 1875
self.n_zealots = 1
self.num_parties = 2
self.planar_c = None
self.propagation = 'standard'
self.q = 25
self.random_dist = False
self.ratio = 0.02
self.reset = False
self.sample_size = 500
self.seat_rule = 'simple'
self.seats = [1]
self.therm_time = 300000
self.threshold = 0.0
self.where_zealots = 'random'
self.zealots_district = None
super().__init__(**kwargs)


class SillyAttribute:

def __getattribute__(self, item):
return "something that definitely is not a 'dest' name of any parameter in the argument parser"


class ArgumentDict(dict):
"""
A class to pass as the 2nd argument for Config instead of parser._option_string_actions,
after getting an item and then an attribute, like arg_dict[argument].dest, it should return something
that is not present in the configuration file, not to raise an error.
"""
def __getitem__(self, key):
return SillyAttribute()


class MockConfigFile:

def __init__(self, **kwargs):
self.name = 'mock config file'
self.config_file = dict({
'avg_deg': 5,
'num_parties': 4,
'propagation': 'majority',
'seat_rule': 'hare',
'therm_time': 15654,
}, **kwargs)

def read(self):
return json.dumps(self.config_file)


class TestConfiguration(unittest.TestCase):

def assertHasAttr(self, obj, intended_attr):
"""
Helper function to assert if the object 'obj' has the attribute 'intended_attr' with a nice message.
"""
test_bool = hasattr(obj, intended_attr)
self.assertTrue(test_bool, msg=f"'{obj}' is lacking an attribute '{intended_attr}'")

def test_num_to_chars(self):
res = num_to_chars(27)
self.assertEqual(res, 'ab')

def test_num_to_chars_error1(self):
def inner():
num_to_chars(-10)
self.assertRaises(ValueError, inner)

def test_num_to_chars_error2(self):
def inner():
num_to_chars(10.0)
self.assertRaises(ValueError, inner)

def test_generate_state_labels(self):
res = generate_state_labels(53)
self.assertListEqual(res,
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'aa', 'ab', 'ac', 'ad', 'ae', 'af', 'ag', 'ah',
'ai', 'aj', 'ak', 'al', 'am', 'an', 'ao', 'ap', 'aq', 'ar', 'as', 'at', 'au', 'av', 'aw',
'ax', 'ay', 'az', 'ba'])

def test_config_basic_class_attributes(self):
# might be silly to test whether some attributes exist, but it might help to think through future changes
# and all the parser actions should be defined as class attributes of Config, so instances know they have them
self.maxDiff = None
self.assertIn('country-wide_system', Config.voting_systems.keys())
self.assertIn('main_district_system', Config.voting_systems.keys())
for action in parser._actions:
if action.dest != 'help':
self.assertHasAttr(Config, action.dest)

self.assertHasAttr(Config, 'zealot_state')
self.assertHasAttr(Config, 'not_zealot_state')
self.assertHasAttr(Config, 'all_states')

self.assertTrue(callable(Config.initialize_states))
self.assertTrue(callable(Config.propagate))
self.assertTrue(callable(Config.mutate))

def test_config_basic_attributes_values(self):
self.maxDiff = None
config = Config(DummyParser(), ArgumentDict())

test_parser = DummyParser(district_sizes=[75 for _ in range(25)], mass_media=0.5)

for action in parser._actions:
if action.dest != 'help':
self.assertEqual(config.__getattribute__(action.dest), test_parser.__getattribute__(action.dest),
msg=f'Attribute {action.dest} different than expected')

self.assertEqual(config.zealot_state, 'a')
self.assertEqual(config.not_zealot_state, 'b')
self.assertEqual(config.all_states, ['a', 'b'])

self.assertTrue(callable(config.initialize_states))
self.assertTrue(callable(config.propagate))
self.assertTrue(callable(config.mutate))

def test_config_attributes_values_alternative_sys(self):
self.maxDiff = None
input_parser = DummyParser()
input_parser.alternative_systems = [
{'name': 'one', 'type': 'basic', 'seat_rule': 'hare', 'threshold': 0.2, 'seats': [2]},
{'name': 'two', 'type': 'merge', 'dist_merging': [1], 'threshold': 0.3},
{'name': 'three', 'type': 'merge', 'seat_rule': 'droop', 'dist_merging': [1] * 20 + [2] * 5, 'seats': [3]},
]
config = Config(input_parser, ArgumentDict())

self.assertListEqual(list(config.voting_systems.keys()),
['country-wide_system', 'main_district_system', 'one', 'two', 'three'])
self.assertDictEqual(config.alternative_systems[0],
{'name': 'one', 'type': 'basic', 'seat_rule': 'hare', 'threshold': 0.2,
'seat_alloc_function': seat_assignment_rules['hare'], 'seats': [2],
'seats_per_district': [2] * 25, 'total_seats': 50})
self.assertDictEqual(config.alternative_systems[1],
{'name': 'two', 'type': 'merge', 'seat_rule': config.seat_rule,
'seat_alloc_function': config.seat_alloc_function, 'threshold': 0.3, 'q': 1,
'total_seats': config.total_seats, 'seats': None, 'dist_merging': [1]})
self.assertDictEqual(config.alternative_systems[2],
{'name': 'three', 'type': 'merge', 'seat_rule': 'droop',
'seat_alloc_function': seat_assignment_rules['droop'], 'threshold': config.threshold,
'q': 2, 'total_seats': 75, 'dist_merging': [1] * 20 + [2] * 5, 'seats': [3],
'seats_per_district': [3] * 25})

def test_config_attributes_values_alternative_sys_error_no_name(self):
input_parser = DummyParser()
input_parser.alternative_systems = [
{'type': 'basic', 'seat_rule': 'hare', 'threshold': 0.2},
]
self.assertRaises(KeyError, Config, input_parser, ArgumentDict())

def test_config_attributes_values_alternative_sys_error_no_type(self):
input_parser = DummyParser()
input_parser.alternative_systems = [
{'name': 'one', 'seat_rule': 'hare'},
]
self.assertRaises(KeyError, Config, input_parser, ArgumentDict())

def test_config_attributes_values_alternative_sys_error_name_duplicate(self):
input_parser = DummyParser()
input_parser.alternative_systems = [
{'name': 'one', 'type': 'basic', 'seat_rule': 'hare', 'threshold': 0.2},
{'name': 'one', 'type': 'basic', 'seat_rule': 'hare', 'threshold': 0.2},
]
self.assertRaises(ValueError, Config, input_parser, ArgumentDict())

def test_config_attributes_values_alternative_sys_error_threshold(self):
input_parser = DummyParser()
input_parser.alternative_systems = [
{'name': 'one', 'type': 'basic', 'seat_rule': 'hare', 'threshold': -0.2},
]
self.assertRaises(ValueError, Config, input_parser, ArgumentDict())

def test_config_attributes_values_alternative_sys_error_seat_rule(self):
input_parser = DummyParser()
input_parser.alternative_systems = [
{'name': 'one', 'type': 'basic', 'seat_rule': 'wrong_seat_rule'},
]
self.assertRaises(ValueError, Config, input_parser, ArgumentDict())

def test_config_attributes_values_alternative_sys_error_seats(self):
input_parser = DummyParser()
input_parser.alternative_systems = [
{'name': 'one', 'type': 'basic', 'seats': []},
]
self.assertRaises(ValueError, Config, input_parser, ArgumentDict())

def test_config_attributes_values_alternative_sys_error_no_dist_merging(self):
input_parser = DummyParser()
input_parser.alternative_systems = [
{'name': 'one', 'type': 'merge'},
]
self.assertRaises(KeyError, Config, input_parser, ArgumentDict())

def test_config_attributes_values_alternative_sys_error_wrong_dist_merging(self):
input_parser = DummyParser()
input_parser.alternative_systems = [
{'name': 'one', 'type': 'merge', 'dist_merging': 1},
]
self.assertRaises(KeyError, Config, input_parser, ArgumentDict())

def test_config_attributes_values_alternative_sys_error_wrong_dist_merging2(self):
input_parser = DummyParser()
input_parser.alternative_systems = [
{'name': 'one', 'type': 'merge', 'dist_merging': []},
]
self.assertRaises(KeyError, Config, input_parser, ArgumentDict())

def test_config_attributes_values_alternative_sys_error_too_short_dist_merging(self):
input_parser = DummyParser()
input_parser.alternative_systems = [
{'name': 'one', 'type': 'merge', 'dist_merging': [1, 2, 3]},
]
self.assertRaises(ValueError, Config, input_parser, ArgumentDict())

def test_config_basic_attributes_values_config_file(self):
input_parser = DummyParser()
input_parser.config_file = MockConfigFile()
config = Config(input_parser, ArgumentDict())
self.assertEqual(config.config_file, 'mock config file')
self.assertEqual(config.avg_deg, 5)
self.assertEqual(config.num_parties, 4)
self.assertEqual(config.propagation, 'majority')
self.assertEqual(config.propagate, majority_propagation)
self.assertEqual(config.seat_rule, 'hare')
self.assertEqual(config.therm_time, 15654)

def test_config_basic_attributes_values_config_file_threshold_error(self):
input_parser = DummyParser()
input_parser.config_file = MockConfigFile(threshold=1.1)
self.assertRaises(ValueError, Config, input_parser, ArgumentDict())

def test_config_attributes_values_threshold_too_low(self):
input_parser = DummyParser(threshold=-1)
self.assertRaises(ValueError, Config, input_parser, ArgumentDict())

def test_config_attributes_values_threshold_too_high(self):
input_parser = DummyParser(threshold=1.5)
self.assertRaises(ValueError, Config, input_parser, ArgumentDict())

def test_config_attributes_values_district_sizes_too_few(self):
input_parser = DummyParser(district_sizes=[1800, 75])
self.assertRaises(ValueError, Config, input_parser, ArgumentDict())

def test_config_attributes_values_district_sizes_too_many(self):
input_parser = DummyParser(district_sizes=[25]*75)
self.assertRaises(ValueError, Config, input_parser, ArgumentDict())

def test_config_attributes_values_district_sizes_wrong_sum(self):
input_parser = DummyParser(district_sizes=[10]*25)
self.assertRaises(ValueError, Config, input_parser, ArgumentDict())

def test_config_attributes_values_n_q_wrong(self):
input_parser = DummyParser(n=1876)
self.assertRaises(ValueError, Config, input_parser, ArgumentDict())

def test_config_attributes_values_district_coords_too_few(self):
input_parser = DummyParser(district_coords=[[1, 2], [3, 4]])
self.assertRaises(ValueError, Config, input_parser, ArgumentDict())

def test_config_attributes_values_district_coords_too_many(self):
input_parser = DummyParser(district_coords=[[1, 2]]*26)
self.assertRaises(ValueError, Config, input_parser, ArgumentDict())

def test_config_attributes_values_parties_too_few(self):
input_parser = DummyParser(num_parties=1)
self.assertRaises(ValueError, Config, input_parser, ArgumentDict())

def test_config_attributes_values_consensus(self):
input_parser = DummyParser(consensus=True)
config = Config(input_parser, ArgumentDict())
self.assertEqual(config.initialize_states, consensus_initial_state)

def test_config_attributes_values_zealots_degree(self):
input_parser = DummyParser(where_zealots='degree', zealots_district=223)
config = Config(input_parser, ArgumentDict())
self.assertDictEqual(config.zealots_config, {'degree_driven': True, 'one_district': False, 'district': None})

def test_config_attributes_values_zealots_dist(self):
input_parser = DummyParser(where_zealots='district', zealots_district=12)
config = Config(input_parser, ArgumentDict())
self.assertDictEqual(config.zealots_config, {'degree_driven': False, 'one_district': True, 'district': 12})

def test_config_attributes_values_zealots_random(self):
input_parser = DummyParser(where_zealots='random')
config = Config(input_parser, ArgumentDict())
self.assertDictEqual(config.zealots_config, {'degree_driven': False, 'one_district': False, 'district': None})

def test_config_attributes_values_mass_media_too_low(self):
input_parser = DummyParser(mass_media=-0.1)
self.assertRaises(ValueError, Config, input_parser, ArgumentDict())

def test_config_attributes_values_mass_media_too_high(self):
input_parser = DummyParser(mass_media=1.1)
self.assertRaises(ValueError, Config, input_parser, ArgumentDict())

def test_config_attributes_values_seats_empty(self):
input_parser = DummyParser(seats=[])
self.assertRaises(ValueError, Config, input_parser, ArgumentDict())

def test_config_attributes_values_wrong_seat_rule(self):
input_parser = DummyParser(seat_rule='wrong seat rule 123')
self.assertRaises(ValueError, Config, input_parser, ArgumentDict())


if __name__ == '__main__':
unittest.main()
4 changes: 2 additions & 2 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def run_experiment(n=None, epsilon=None, sample_size=None, therm_time=None, n_ze
ratio=config.ratio, planar_const=config.planar_c, euclidean=config.euclidean,
state_generator=config.initialize_states, random_dist=config.random_dist,
initial_state=config.not_zealot_state, all_states=config.all_states)
init_g = add_zealots(init_g, n_zealots, zealot_state=config.zealot_state, **config.zealots_config)
init_g = add_zealots(init_g, n_zealots, config.zealot_state, **config.zealots_config)

if not silent:
link_fraction, link_ratio = compute_edge_ratio(init_g)
Expand All @@ -55,7 +55,7 @@ def run_experiment(n=None, epsilon=None, sample_size=None, therm_time=None, n_ze
g.vs()["state"] = config.initialize_states(n, all_states=config.all_states, state=config.not_zealot_state)
# we have to reset zealots, otherwise they would have states different than 'zealot_state'
g.vs()["zealot"] = np.zeros(n)
g = add_zealots(g, n_zealots, zealot_state=config.zealot_state, **config.zealots_config)
g = add_zealots(g, n_zealots, config.zealot_state, **config.zealots_config)
g = run_thermalization_simple(config, g, epsilon, therm_time, n=n)

g = run_simulation(config, g, epsilon, n * config.mc_steps, n=n)
Expand Down
Loading

0 comments on commit ab4ea1e

Please sign in to comment.