symian 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,66 @@
1
+ require 'test_helper'
2
+
3
+ require 'symian/reference_configuration'
4
+
5
+
6
+ describe Symian::Configuration do
7
+
8
+ context 'simulation-related parameters' do
9
+
10
+ it 'should correctly load simulation start' do
11
+ with_reference_config do |conf|
12
+ conf.start_time.must_equal START_TIME
13
+ end
14
+ end
15
+
16
+ it 'should correctly load simulation duration' do
17
+ with_reference_config do |conf|
18
+ conf.duration.must_equal DURATION
19
+ end
20
+ end
21
+
22
+ it 'should correctly load simulation end time' do
23
+ with_reference_config do |conf|
24
+ conf.end_time.must_equal START_TIME + DURATION
25
+ end
26
+ end
27
+
28
+ it 'should correctly load warmup phase duration' do
29
+ with_reference_config do |conf|
30
+ conf.warmup_duration.must_equal WARMUP_DURATION
31
+ end
32
+ end
33
+
34
+ it 'should initialize incident generation' do
35
+ with_reference_config do |conf|
36
+ conf.incident_generation.must_equal INCIDENT_GENERATION
37
+ end
38
+ end
39
+
40
+ it 'should initialize support groups' do
41
+ with_reference_config do |conf|
42
+ conf.support_groups.must_equal SUPPORT_GROUPS
43
+ end
44
+ end
45
+
46
+ it 'should initialize transition matrix' do
47
+ with_reference_config do |conf|
48
+ conf.transition_matrix.must_equal TRANSITION_MATRIX
49
+ end
50
+ end
51
+
52
+ end
53
+
54
+ context 'cloning mechanism' do
55
+ it 'should correctly clone w/ other ops' do
56
+ with_reference_config do |conf|
57
+ new_ops = (1..conf.support_groups.size).to_a
58
+ new_conf = conf.reallocate_ops_and_clone(new_ops)
59
+ new_conf.support_groups.zip(new_ops) do |(k,v),num_ops|
60
+ v[:operators][:number].must_equal num_ops
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ end
@@ -0,0 +1,52 @@
1
+ require 'test_helper'
2
+
3
+ require 'symian/reference_configuration'
4
+ require 'symian/cost_analyzer'
5
+
6
+
7
+ describe Symian::CostAnalyzer do
8
+
9
+ context 'operations' do
10
+ it 'should correctly calculate daily operations' do
11
+ EXAMPLE_KPIS = { :mttr => 9000, :micd => 450 }
12
+
13
+ with_reference_config do |conf|
14
+ ca = Symian::CostAnalyzer.new(conf)
15
+ res = ca.evaluate(EXAMPLE_KPIS)
16
+ res[:operations].must_equal((25_000 + 30_000 + 40_000) / 365.0)
17
+ end
18
+ end
19
+ end
20
+
21
+ context 'contracting' do
22
+ it 'should work if no contracting function is provided' do
23
+ cost_analysis_wo_contracting = COST_ANALYSIS.reject {|x| x == :contracting }
24
+ with_reference_config(cost_analysis: cost_analysis_wo_contracting) do |conf|
25
+ Symian::CostAnalyzer.new(conf)
26
+ end
27
+ end
28
+ end
29
+
30
+ context 'drift' do
31
+ it 'should work if no drift function is provided' do
32
+ cost_analysis_wo_drift = COST_ANALYSIS.reject {|x| x == :drift }
33
+ with_reference_config(cost_analysis: cost_analysis_wo_drift) do |conf|
34
+ Symian::CostAnalyzer.new(conf)
35
+ end
36
+ end
37
+ end
38
+
39
+ # it 'should work if penalty function returns something' do
40
+ # evaluator = with_reference_config do |conf|
41
+ # SISFC::Evaluator.new(conf)
42
+ # end
43
+ # evaluator.evaluate_business_impact({ mttr: 0.075 }, nil, EXAMPLE_ALLOCATION)
44
+ # end
45
+
46
+ # it 'should work if penalty function returns nil' do
47
+ # evaluator = with_reference_config do |conf|
48
+ # SISFC::Evaluator.new(conf)
49
+ # end
50
+ # evaluator.evaluate_business_impact({ mttr: 0.025 }, nil, EXAMPLE_ALLOCATION)
51
+ # end
52
+ end
@@ -0,0 +1,27 @@
1
+ require 'test_helper'
2
+
3
+ require 'symian/generator'
4
+ require 'symian/event'
5
+
6
+
7
+ describe Symian::IncidentGenerator do
8
+
9
+ let(:simulation) { MiniTest::Mock.new }
10
+
11
+
12
+ it 'should generate incidents with random arrival times' do
13
+ gen = Symian::IncidentGenerator.new(simulation,
14
+ :type => :sequential_random_variable,
15
+ :source => { :first_value => Time.now,
16
+ :seed => (Process.pid / rand).to_i,
17
+ :distribution => :discrete_uniform,
18
+ :max_value => 100 })
19
+ simulation.expect(:new_event, nil, [ Symian::Event::ET_INCIDENT_ARRIVAL, Symian::Incident, Time, nil ])
20
+ gen.generate
21
+ end
22
+
23
+
24
+ it 'should generate incidents with arrival times from traces'
25
+
26
+
27
+ end
@@ -0,0 +1,104 @@
1
+ require 'test_helper'
2
+
3
+ require 'symian/incident'
4
+
5
+ describe Symian::Incident do
6
+
7
+ it 'should ignore invalid parameters passed to the constructor' do
8
+ Symian::Incident.new(1, Time.now, :some_invalid_param => "dummy")
9
+ end
10
+
11
+ it 'should not be closed unless closure time is provided' do
12
+ inc = Symian::Incident.new(1, Time.now)
13
+ inc.closed?.must_equal false
14
+ end
15
+
16
+ it 'should be closed if closure time is provided' do
17
+ inc = Symian::Incident.new(1, Time.now)
18
+ inc.closure_time = Time.now
19
+ inc.closed?.must_equal true
20
+ end
21
+
22
+ it 'should have a nil TTR if still open' do
23
+ inc = Symian::Incident.new(1, Time.now)
24
+ inc.ttr.must_be_nil
25
+ end
26
+
27
+ it 'should have a valid TTR if closed' do
28
+ arrival_time = Time.now
29
+ closure_time = Time.now + 1.hour
30
+ inc = Symian::Incident.new(1, arrival_time,
31
+ :closure_time => closure_time)
32
+ inc.ttr.must_equal 1.hour
33
+ end
34
+
35
+ it 'should correctly manage tracking information' do
36
+ inc = Symian::Incident.new(1, Time.now)
37
+ tis = [ { :type => :queue,
38
+ :at => Time.now,
39
+ :duration => 50.seconds,
40
+ :sg => 'SG1' },
41
+ { :type => :work,
42
+ :at => Time.now + 1.hour,
43
+ :duration => 2.hours,
44
+ :sg => 'SG2' },
45
+ { :type => :suspend,
46
+ :at => Time.now + 20.minutes,
47
+ :duration => 30.minutes,
48
+ :sg => 'SG3' } ]
49
+ tis.each do |ti|
50
+ inc.add_tracking_information(ti)
51
+ end
52
+ i = 0
53
+ inc.with_tracking_information do |ti|
54
+ ti.must_equal tis[i]
55
+ i += 1
56
+ end
57
+ end
58
+
59
+ it 'should correctly calculate time spent at last support group' do
60
+ inc = Symian::Incident.new(1, Time.now)
61
+ now = Time.now
62
+
63
+ tis = [ { :type => :queue,
64
+ :at => now,
65
+ :duration => 1.hour,
66
+ :sg => 'SG1' },
67
+ { :type => :work,
68
+ :at => now + 1.hour,
69
+ :duration => 2.hours,
70
+ :sg => 'SG1' },
71
+ { :type => :queue,
72
+ :at => now + 3.hours,
73
+ :duration => 1.hour,
74
+ :sg => 'SG2' },
75
+ { :type => :work,
76
+ :at => now + 4.hours,
77
+ :duration => 2.hours,
78
+ :sg => 'SG2' },
79
+ { :type => :queue,
80
+ :at => now + 6.hours,
81
+ :duration => 1.hour,
82
+ :sg => 'SG3' },
83
+ { :type => :work,
84
+ :at => now + 7.hours,
85
+ :duration => 2.hours,
86
+ :sg => 'SG3' },
87
+ { :type => :suspend,
88
+ :at => now + 9.hours,
89
+ :duration => 30.minutes,
90
+ :sg => 'SG3' },
91
+ { :type => :work,
92
+ :at => now + 9.hours + 30.minutes,
93
+ :duration => 30.minutes,
94
+ :sg => 'SG3' } ]
95
+
96
+ tis.each do |ti|
97
+ inc.add_tracking_information(ti)
98
+ end
99
+
100
+ inc.total_time_at_last_sg.must_equal 4.hours
101
+ inc.queue_time_at_last_sg.must_equal 1.hour
102
+ end
103
+
104
+ end
@@ -0,0 +1,60 @@
1
+ require 'test_helper'
2
+
3
+ require 'symian/incident'
4
+ require 'symian/operator'
5
+
6
+
7
+ describe Symian::Operator do
8
+
9
+ it 'should support :workshift => :all_day_long shortcut' do
10
+ o = Symian::Operator.new(1, 1, :workshift => :all_day_long)
11
+ o.workshift.must_equal Symian::WorkShift::WORKSHIFT_24x7
12
+ end
13
+
14
+
15
+ it 'should work on minor incidents until escalation' do
16
+ assignment_time = Time.now # incident is assigned now
17
+ needed_work_time = 1.hour # incident requires 1 hour of work
18
+ arrival_time = assignment_time - 1.hour # incident arrived one hour ago
19
+ expected_escalation_time = assignment_time + 1.hour # incident should be closed in one hour
20
+
21
+ o = Symian::Operator.new(1, 1)
22
+ i = Symian::Incident.new(1, arrival_time)
23
+
24
+ o.assign(i, { :needed_work_time => needed_work_time }, assignment_time).must_equal [ :incident_escalation, expected_escalation_time ]
25
+ end
26
+
27
+
28
+ it 'should work on major incidents until time of shift' do
29
+ assignment_time = Time.now # incident is assigned now
30
+ needed_work_time = 2.hours # incident requires 2 hours of work
31
+ arrival_time = assignment_time - 1.hour # incident arrived one hour ago
32
+ workshift_start = assignment_time - 7.hours # operator workshift started 7 hours ago
33
+ workshift_end = assignment_time + 1.hour # operator workshift ends in 1 hour
34
+
35
+ o = Symian::Operator.new(1, 1,
36
+ :workshift => Symian::WorkShift.new(:custom,
37
+ :start_time => workshift_start,
38
+ :end_time => workshift_end))
39
+ i = Symian::Incident.new(1, arrival_time)
40
+
41
+ o.assign(i, { :needed_work_time => needed_work_time }, assignment_time).must_equal [ :operator_off_duty, workshift_end ]
42
+ end
43
+
44
+
45
+ it 'should have specialization factors skewing its productivity' do
46
+ assignment_time = Time.now # incident is assigned now
47
+ needed_work_time = 2.hours # incident requires 2 hours of work
48
+ arrival_time = assignment_time - 1.hour # incident arrived one hour ago
49
+ expected_escalation_time = assignment_time + 1.hour # incident should be closed in one hour
50
+
51
+ o = Symian::Operator.new(1, 1,
52
+ :specialization => { :web => 2.0 }) # 2x specialization on 'web' incidents
53
+
54
+ i = Symian::Incident.new(1, arrival_time, :category => :web)
55
+
56
+ o.assign(i, { :needed_work_time => needed_work_time }, assignment_time).must_equal [ :incident_escalation, expected_escalation_time ]
57
+ end
58
+
59
+ end
60
+
@@ -0,0 +1,107 @@
1
+ require 'symian/configuration'
2
+
3
+ START_TIME = Time.utc(1978, 'Aug', 12, 14, 30, 0)
4
+ DURATION = 1.minute
5
+ WARMUP_DURATION = 10.seconds
6
+ SIMULATION_CHARACTERIZATION = <<END
7
+ start_time Time.utc(1978, 'Aug', 12, 14, 30, 0)
8
+ duration 1.minute
9
+ warmup_duration 10.seconds
10
+ END
11
+
12
+ INCIDENT_GENERATION_CHARACTERIZATION = <<END
13
+ incident_generation \
14
+ :type => :sequential_random_variable,
15
+ :source => {
16
+ :first_value => Time.utc(1978, 'Aug', 12, 14, 31, 0),
17
+ :distribution => :exponential,
18
+ :mean => 1/0.0015
19
+ }
20
+ END
21
+
22
+ SUPPORT_GROUPS_CHARACTERIZATION = <<END
23
+ support_groups \
24
+ 'SG1' => { :work_time => { :distribution => :exponential, :mean => 227370 },
25
+ :operators => { :number => 1, :workshift => :all_day_long } },
26
+ 'SG2' => { :work_time => { :distribution => :exponential, :mean => 1980 },
27
+ :operators => { :number => 1, :workshift => :all_day_long } },
28
+ 'SG3' => { :work_time => { :distribution => :exponential, :mean => 360 },
29
+ :operators => { :number => 1, :workshift => :all_day_long } }
30
+ END
31
+
32
+ TRANSITION_MATRIX_CHARACTERIZATION = <<END
33
+ transition_matrix %q{
34
+ From/To,SG1,SG2,SG3,Out
35
+ In,25,50,25,0
36
+ SG1,0,10,70,20
37
+ SG2,5,0,45,45
38
+ SG3,10,20,0,70
39
+ }
40
+ END
41
+
42
+ COST_ANALYSIS_CHARACTERIZATION = <<END
43
+ cost_analysis \
44
+ :operations => [
45
+ { :sg_name => 'SG1', :operator_salary => 30_000 },
46
+ { :sg_name => 'SG2', :operator_salary => 40_000 },
47
+ { :sg_name => 'SG3', :operator_salary => 25_000 },
48
+ ],
49
+ :contracting => lambda { |kpis|
50
+ kpis[:mttr] > 9000 ? 1500 : 0.0
51
+ },
52
+ :drift => lambda { |kpis|
53
+ target = 500
54
+ delta = target - kpis[:micd]
55
+ if delta > 0.0
56
+ 1500.0 * (2.0 / Math::PI) * Math::atan(10.0 * delta / target)
57
+ else
58
+ 0.0
59
+ end
60
+ }
61
+ END
62
+
63
+
64
+ # this is the whole reference configuration
65
+ # (useful for spec'ing configuration.rb)
66
+ REFERENCE_CONFIGURATION =
67
+ SIMULATION_CHARACTERIZATION +
68
+ INCIDENT_GENERATION_CHARACTERIZATION +
69
+ SUPPORT_GROUPS_CHARACTERIZATION +
70
+ TRANSITION_MATRIX_CHARACTERIZATION +
71
+ COST_ANALYSIS_CHARACTERIZATION
72
+
73
+ evaluator = Object.new
74
+ evaluator.extend Symian::Configurable
75
+ evaluator.instance_eval(REFERENCE_CONFIGURATION)
76
+
77
+ # these are preprocessed portions of the reference configuration
78
+ # (useful for spec'ing everything else)
79
+ INCIDENT_GENERATION = evaluator.incident_generation
80
+ SUPPORT_GROUPS = evaluator.support_groups
81
+ TRANSITION_MATRIX = evaluator.transition_matrix
82
+ COST_ANALYSIS = evaluator.cost_analysis
83
+
84
+
85
+ def with_reference_config(opts={})
86
+ # create temporary file with reference configuration
87
+ tf = Tempfile.open('REFERENCE_CONFIGURATION')
88
+ begin
89
+ tf.write(REFERENCE_CONFIGURATION)
90
+ tf.close
91
+
92
+ # create a configuration object from the reference configuration file
93
+ conf = Symian::Configuration.load_from_file(tf.path)
94
+
95
+ # apply any change from the opts parameter and validate the modified configuration
96
+ opts.each do |k,v|
97
+ conf.send(k, v)
98
+ end
99
+ conf.validate
100
+
101
+ # pass the configuration object to the block
102
+ yield conf
103
+ ensure
104
+ # delete temporary file
105
+ tf.delete
106
+ end
107
+ end
@@ -0,0 +1,111 @@
1
+ require 'test_helper'
2
+
3
+ require 'symian/incident'
4
+ require 'symian/support_group'
5
+ require 'symian/work_shift'
6
+
7
+
8
+ describe Symian::SupportGroup do
9
+
10
+ before :each do
11
+ @simulation = MiniTest::Mock.new
12
+ end
13
+
14
+
15
+ it 'should be creatable with one operator group' do
16
+ sg = Symian::SupportGroup.new('SG',
17
+ @simulation,
18
+ { :distribution => :exponential, :mean => 5 },
19
+ { :number => 3, :workshift => Symian::WorkShift.new(:all_day_long) })
20
+ start_time = Time.now
21
+ sg.initialize_at(start_time)
22
+ sg.operators.size.must_equal 3
23
+ end
24
+
25
+
26
+ it 'should be creatable with several operator groups' do
27
+ sg = Symian::SupportGroup.new('SG',
28
+ @simulation,
29
+ { :distribution => :exponential, :mean => 5 },
30
+ [ { :number => 3, :workshift => Symian::WorkShift.new(:all_day_long) },
31
+ { :number => 4, :workshift => Symian::WorkShift.new(:all_day_long) },
32
+ { :number => 3, :workshift => Symian::WorkShift.new(:all_day_long) } ])
33
+ start_time = Time.now
34
+ sg.initialize_at(start_time)
35
+ sg.operators.size.must_equal 10
36
+ end
37
+
38
+
39
+ it 'should allow incident reassignments' do
40
+ ws = Symian::WorkShift.new(:custom,
41
+ :start_time => Time.utc(2009, 'Jan', 1, 8, 0, 0),
42
+ :end_time => Time.utc(2009, 'Jan', 1, 16, 0, 0))
43
+ sg = Symian::SupportGroup.new('SG',
44
+ @simulation,
45
+ { :distribution => :constant, :value => 10.hours },
46
+ [ { :number => 1, :workshift => ws } ])
47
+ start_time = Time.utc(2009, 'Jan', 1, 8, 0, 0)
48
+ incident_arrival = start_time + 2.hours
49
+
50
+ @simulation.expect(:new_event,
51
+ nil,
52
+ [ Symian::Event::ET_OPERATOR_LEAVING, String, incident_arrival + 6.hours, 'SG'])
53
+
54
+ sg.initialize_at(start_time)
55
+ i = Symian::Incident.new(0, incident_arrival,
56
+ :category => 'normal',
57
+ :priority => 0) # not supported at the moment
58
+
59
+ # first increase queue size,...
60
+ @simulation.expect(:new_event, nil, [Symian::Event::ET_SUPPORT_GROUP_QUEUE_SIZE_CHANGE, 1, incident_arrival, 'SG'])
61
+ # ...then decrease it, ...
62
+ @simulation.expect(:new_event, nil, [Symian::Event::ET_SUPPORT_GROUP_QUEUE_SIZE_CHANGE, 0, incident_arrival, 'SG'])
63
+ # ...assign it to an operator, ...
64
+ @simulation.expect(:new_event, nil, [Symian::Event::ET_INCIDENT_ASSIGNMENT, Array, incident_arrival, 'SG'])
65
+ @simulation.expect(:new_event, nil, [Symian::Event::ET_OPERATOR_ACTIVITY_STARTS, Array, incident_arrival, 'SG'])
66
+ # ...and finally escalate the incident.
67
+ @simulation.expect(:new_event, nil, [Symian::Event::ET_OPERATOR_ACTIVITY_FINISHES, Array, incident_arrival + 6.hours, 'SG'])
68
+ @simulation.expect(:new_event, nil, [Symian::Event::ET_INCIDENT_RESCHEDULING, Array, incident_arrival + 6.hours, 'SG'])
69
+
70
+ sg.new_incident(i, incident_arrival)
71
+
72
+ i.visited_support_groups.must_equal 1
73
+ end
74
+
75
+
76
+ it 'should accept new incidents' do
77
+ sg = Symian::SupportGroup.new('SG',
78
+ @simulation,
79
+ { :distribution => :constant, :value => 500 },
80
+ [ { :number => 3, :workshift => Symian::WorkShift.new(:all_day_long) } ])
81
+ start_time = Time.now
82
+ sg.initialize_at(start_time)
83
+ incident_arrival = start_time + 3600 # after 1 hour
84
+ i = Symian::Incident.new(0, incident_arrival,
85
+ :category => 'normal',
86
+ :priority => 0) # not supported at the moment
87
+
88
+ # first increase queue size,...
89
+ @simulation.expect(:new_event, nil, [Symian::Event::ET_SUPPORT_GROUP_QUEUE_SIZE_CHANGE, 1, incident_arrival, 'SG'])
90
+ # ...then decrease it, ...
91
+ @simulation.expect(:new_event, nil, [Symian::Event::ET_SUPPORT_GROUP_QUEUE_SIZE_CHANGE, 0, incident_arrival, 'SG'])
92
+ # ...assign it to an operator, ...
93
+ @simulation.expect(:new_event, nil, [Symian::Event::ET_INCIDENT_ASSIGNMENT, Array, incident_arrival, 'SG'])
94
+ @simulation.expect(:new_event, nil, [Symian::Event::ET_OPERATOR_ACTIVITY_STARTS, Array, incident_arrival, 'SG'])
95
+ # ...and finally escalate the incident.
96
+ @simulation.expect(:new_event, nil, [Symian::Event::ET_OPERATOR_ACTIVITY_FINISHES, Array, incident_arrival + 500, 'SG'])
97
+ @simulation.expect(:new_event, nil, [Symian::Event::ET_INCIDENT_ESCALATION, Symian::Incident, incident_arrival + 500, 'SG'])
98
+
99
+ sg.new_incident(i, incident_arrival)
100
+
101
+ i.visited_support_groups.must_equal 1
102
+ end
103
+
104
+ # it 'should handle operators going home'
105
+
106
+ # it 'should handle operators coming back'
107
+
108
+ # it 'should handle operators finishing their work'
109
+
110
+ end
111
+