Merge pull request #413 from knu/improve_diagram

Improve agents diagram.

Andrew Cantino 10 lat temu
rodzic
commit
9d0695fc76
2 zmienionych plików z 205 dodań i 40 usunięć
  1. 137 25
      app/helpers/dot_helper.rb
  2. 68 15
      spec/helpers/dot_helper_spec.rb

+ 137 - 25
app/helpers/dot_helper.rb

@@ -14,38 +14,150 @@ module DotHelper
14 14
     end
15 15
   end
16 16
 
17
-  private
17
+  class DotDrawer
18
+    def initialize(vars = {})
19
+      @dot = ''
20
+      vars.each { |name, value|
21
+        # Import variables as methods
22
+        define_singleton_method(name) { value }
23
+      }
24
+    end
25
+
26
+    def to_s
27
+      @dot
28
+    end
29
+
30
+    def self.draw(*args, &block)
31
+      drawer = new(*args)
32
+      drawer.instance_exec(&block)
33
+      drawer.to_s
34
+    end
35
+
36
+    def raw(string)
37
+      @dot << string
38
+    end
39
+
40
+    def escape(string)
41
+      # Backslash escaping seems to work for the backslash itself,
42
+      # though it's not documented in the DOT language docs.
43
+      string.gsub(/[\\"\n]/,
44
+                  "\\" => "\\\\",
45
+                  "\"" => "\\\"",
46
+                  "\n" => "\\n")
47
+    end
48
+
49
+    def id(value)
50
+      case string = value.to_s
51
+      when /\A(?!\d)\w+\z/, /\A(?:\.\d+|\d+(?:\.\d*)?)\z/
52
+        raw string
53
+      else
54
+        raw '"'
55
+        raw escape(string)
56
+        raw '"'
57
+      end
58
+    end
59
+
60
+    def attr_list(attrs = nil)
61
+      return if attrs.nil?
62
+      attrs = attrs.select { |key, value| value.present? }
63
+      return if attrs.empty?
64
+      raw '['
65
+      attrs.each_with_index { |(key, value), i|
66
+        raw ',' if i > 0
67
+        id key
68
+        raw '='
69
+        id value
70
+      }
71
+      raw ']'
72
+    end
18 73
 
19
-  def dot_id(string)
20
-    # Backslash escaping seems to work for the backslash itself,
21
-    # despite the DOT language document.
22
-    '"%s"' % string.gsub(/\\/, "\\\\\\\\").gsub(/"/, "\\\\\"")
74
+    def node(id, attrs = nil)
75
+      id id
76
+      attr_list attrs
77
+      raw ';'
78
+    end
79
+
80
+    def edge(from, to, attrs = nil, op = '->')
81
+      id from
82
+      raw op
83
+      id to
84
+      attr_list attrs
85
+      raw ';'
86
+    end
87
+
88
+    def statement(ids, attrs = nil)
89
+      Array(ids).each_with_index { |id, i|
90
+        raw ' ' if i > 0
91
+        id id
92
+      }
93
+      attr_list attrs
94
+      raw ';'
95
+    end
96
+
97
+    def block(title, &block)
98
+      raw title
99
+      raw '{'
100
+      block.call
101
+      raw '}'
102
+    end
23 103
   end
24 104
 
25
-  def disabled_label(agent)
26
-    agent.disabled? ? dot_id(agent.name + " (Disabled)") : dot_id(agent.name)
105
+  private
106
+
107
+  def draw(vars = {}, &block)
108
+    DotDrawer.draw(vars, &block)
27 109
   end
28 110
 
29 111
   def agents_dot(agents, rich = false)
30
-    "digraph foo {".tap { |dot|
31
-      agents.each.with_index do |agent, index|
32
-        if rich
33
-          dot << '%s[URL=%s];' % [disabled_label(agent), dot_id(agent_path(agent.id))]
34
-        else
35
-          dot << '%s;' % disabled_label(agent)
36
-        end
37
-        agent.receivers.each do |receiver|
38
-          next unless agents.include?(receiver)
39
-          dot << "%s->%s%s;" % [
40
-            disabled_label(agent),
41
-            disabled_label(receiver),
42
-            if rich
43
-              '[style=dashed]' unless receiver.propagate_immediately
44
-            end
45
-          ]
46
-        end
112
+    draw(agents: agents,
113
+         agent_id: ->agent { 'a%d' % agent.id },
114
+         agent_label: ->agent {
115
+           if agent.disabled?
116
+             '%s (Disabled)' % agent.name
117
+           else
118
+             agent.name
119
+           end.gsub(/(.{20}\S*)\s+/) {
120
+             # Fold after every 20+ characters
121
+             $1 + "\n"
122
+           }
123
+         },
124
+         agent_url: ->agent { agent_path(agent.id) },
125
+         rich: rich) {
126
+      @disabled = '#999999'
127
+
128
+      def agent_node(agent)
129
+        node(agent_id[agent],
130
+             label: agent_label[agent],
131
+             URL: (agent_url[agent] if rich),
132
+             style: ('rounded,dashed' if agent.disabled?),
133
+             color: (@disabled if agent.disabled?),
134
+             fontcolor: (@disabled if agent.disabled?))
135
+      end
136
+
137
+      def agent_edge(agent, receiver)
138
+        edge(agent_id[agent],
139
+             agent_id[receiver],
140
+             style: ('dashed' unless receiver.propagate_immediately),
141
+             color: (@disabled if agent.disabled? || receiver.disabled?))
47 142
       end
48
-      dot << "}"
143
+
144
+      block('digraph foo') {
145
+        # statement 'graph', rankdir: 'LR'
146
+        statement 'node',
147
+                  shape: 'box',
148
+                  style: 'rounded',
149
+                  target: '_blank',
150
+                  fontsize: 10,
151
+                  fontname: ('Helvetica' if rich)
152
+
153
+        agents.each.with_index { |agent, index|
154
+          agent_node(agent)
155
+
156
+          agent.receivers.each { |receiver|
157
+            agent_edge(agent, receiver) if agents.include?(receiver)
158
+          }
159
+        }
160
+      }
49 161
     }
50 162
   end
51 163
 end

+ 68 - 15
spec/helpers/dot_helper_spec.rb

@@ -1,12 +1,6 @@
1 1
 require 'spec_helper'
2 2
 
3 3
 describe DotHelper do
4
-  describe "#dot_id" do
5
-    it "properly escapes double quotaion and backslash" do
6
-      dot_id('hello\\"').should == '"hello\\\\\\""'
7
-    end
8
-  end
9
-
10 4
   describe "with example Agents" do
11 5
     class Agents::DotFoo < Agent
12 6
       default_schedule "2pm"
@@ -30,18 +24,77 @@ describe DotHelper do
30 24
     end
31 25
 
32 26
     describe "#agents_dot" do
27
+      before do
28
+        @agents = [
29
+          @foo = Agents::DotFoo.new(name: "foo").tap { |agent|
30
+            agent.user = users(:bob)
31
+            agent.save!
32
+          },
33
+
34
+          @bar1 = Agents::DotBar.new(name: "bar1").tap { |agent|
35
+            agent.user = users(:bob)
36
+            agent.sources << @foo
37
+            agent.save!
38
+          },
39
+
40
+          @bar2 = Agents::DotBar.new(name: "bar2").tap { |agent|
41
+            agent.user = users(:bob)
42
+            agent.sources << @foo
43
+            agent.propagate_immediately = true
44
+            agent.disabled = true
45
+            agent.save!
46
+          },
47
+
48
+          @bar3 = Agents::DotBar.new(name: "bar3").tap { |agent|
49
+            agent.user = users(:bob)
50
+            agent.sources << @bar2
51
+            agent.save!
52
+          },
53
+        ]
54
+      end
55
+
33 56
       it "generates a DOT script" do
34
-        @foo = Agents::DotFoo.new(:name => "foo")
35
-        @foo.user = users(:bob)
36
-        @foo.save!
57
+        agents_dot(@agents).should =~ %r{
58
+          \A
59
+          digraph \s foo \{
60
+            node \[ [^\]]+ \];
61
+            (?<foo>\w+) \[label=foo\];
62
+            \k<foo> -> (?<bar1>\w+) \[style=dashed\];
63
+            \k<foo> -> (?<bar2>\w+) \[color="\#999999"\];
64
+            \k<bar1> \[label=bar1\];
65
+            \k<bar2> \[label="bar2 \s \(Disabled\)",style="rounded,dashed",color="\#999999",fontcolor="\#999999"\];
66
+            \k<bar2> -> (?<bar3>\w+) \[style=dashed,color="\#999999"\];
67
+            \k<bar3> \[label=bar3\];
68
+          \}
69
+          \z
70
+        }x
71
+      end
37 72
 
38
-        @bar = Agents::DotBar.new(:name => "bar")
39
-        @bar.user = users(:bob)
40
-        @bar.sources << @foo
41
-        @bar.save!
73
+      it "generates a richer DOT script" do
74
+        agents_dot(@agents, true).should =~ %r{
75
+          \A
76
+          digraph \s foo \{
77
+            node \[ [^\]]+ \];
78
+            (?<foo>\w+) \[label=foo,URL="#{Regexp.quote(agent_path(@foo))}"\];
79
+            \k<foo> -> (?<bar1>\w+) \[style=dashed\];
80
+            \k<foo> -> (?<bar2>\w+) \[color="\#999999"\];
81
+            \k<bar1> \[label=bar1,URL="#{Regexp.quote(agent_path(@bar1))}"\];
82
+            \k<bar2> \[label="bar2 \s \(Disabled\)",URL="#{Regexp.quote(agent_path(@bar2))}",style="rounded,dashed",color="\#999999",fontcolor="\#999999"\];
83
+            \k<bar2> -> (?<bar3>\w+) \[style=dashed,color="\#999999"\];
84
+            \k<bar3> \[label=bar3,URL="#{Regexp.quote(agent_path(@bar3))}"\];
85
+          \}
86
+          \z
87
+        }x
88
+      end
89
+    end
90
+  end
42 91
 
43
-        agents_dot([@foo, @bar]).should == 'digraph foo {"foo";"foo"->"bar";"bar";}'
44
-        agents_dot([@foo, @bar], true).should == 'digraph foo {"foo"[URL="/agents/%d"];"foo"->"bar"[style=dashed];"bar"[URL="/agents/%d"];}' % [@foo.id, @bar.id]
92
+  describe DotHelper::DotDrawer do
93
+    describe "#id" do
94
+      it "properly escapes double quotaion and backslash" do
95
+        DotHelper::DotDrawer.draw(foo: "") {
96
+          id('hello\\"')
97
+        }.should == '"hello\\\\\\""'
45 98
       end
46 99
     end
47 100
   end