symian 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+