Browse Source

Update parser and Jinja template for newer JATS result data model

Needless to say, the HTML template is still horrible and needs someone
with better HTML/CSS skills.

The Python parser is still not really correct, but at least does not
depend on function names.
Alois Mahdal 6 years ago
parent
commit
5c1a5dc9ee
3 changed files with 184 additions and 144 deletions
  1. 2
    2
      packaging/template.spec
  2. 63
    16
      src/jinja2/html.j2
  3. 119
    126
      src/python/jat.py

+ 2
- 2
packaging/template.spec View File

@@ -24,8 +24,8 @@ all other sub-packages.
24 24
 Requires: %shellfu_req
25 25
 Requires: %saturnin_req
26 26
 Requires: shellfu-bash
27
-Requires: shellfu-bash-jat >= 0.0.0
28
-Requires: shellfu-bash-jat < 0.0.1
27
+Requires: shellfu-bash-jat >= 0.0.1
28
+Requires: shellfu-bash-jat < 0.0.2
29 29
 Requires: shellfu-bash-pretty
30 30
 Summary: JAT harness and test discovery
31 31
 %description minimal

+ 63
- 16
src/jinja2/html.j2 View File

@@ -1,18 +1,67 @@
1
+{% macro assert(A) -%}
2
+<div class="event assert v_{{A.verdict}}">
3
+    &nbsp;
4
+    <div class="hint">
5
+        <span class="verdict">{{A.verdict}}</span>{{A.hint}}
6
+    </div>
7
+    {% if A.data %}<pre class="data">{{A.data}}</pre>{% endif %}
8
+ </div>
9
+{%- endmacro %}
10
+
11
+{% macro message(M) -%}
12
+<div class="event message s_{{M.severity}}">
13
+    <div class="severity">{{M.severity}}</div>
14
+    <pre class="message">{{M.message}}</pre>
15
+    {% if M.data %}<pre class="data">{{M.data}}</pre>{% endif %}
16
+ </div>
17
+{%- endmacro %}
18
+
19
+{% macro phase(P) -%}
20
+<div class="phase v_{{P.verdict}}">
21
+    <div class="name p_{{P.type}}">
22
+        <span class="verdict">{{P.verdict}}</span>{{P.name}}
23
+    </div>
24
+    {% for e in P.children %}
25
+        {% if e.is_assert %}
26
+            {{ assert(e) }}
27
+        {% elif e.is_message %}
28
+            {{ message(e) }}
29
+        {% endif %}
30
+    {% endfor %}
31
+</div>
32
+{%- endmacro %}
33
+
1 34
 <!doctype html>
2 35
 <html>
3 36
     <head>
4 37
         <title>JAT report {{ session.sessid }}</title>
5 38
 
6 39
         <style type="text/css">
7
-            body { color: gray; background-color: black }
8
-            .lint { background-color: orange }
9
-            .phase { font-weight: bold }
10
-            .p_setup { color: blue }
40
+            body {
41
+                color: gray;
42
+                background-color: black;
43
+                margin: 2em;
44
+            }
45
+            .lint { background-color: orange; padding: 1em }
46
+            .phase {
47
+                padding-left: 1em;
48
+                margin-top: 2em;
49
+                margin-bottom: 2em;
50
+            }
51
+            .phase .name { font-weight: bold }
52
+            .phase .verdict { float: right }
53
+            .p_setup { color: orange }
11 54
             .p_test { color: yellow }
12 55
             .p_diag { color: lightblue }
13 56
             .p_cleanup { color: blue }
14
-            .e_PASS { color: green }
15
-            .e_FAIL { color: red }
57
+            .event { border: 1px solid #333 }
58
+            .v_PASS { color: green }
59
+            .v_FAIL { color: red }
60
+            .v_UNKNOWN { color: orange }
61
+            .severity { float: right }
62
+            .s_INFO { color: silver }
63
+            .s_WARNING { color: orange }
64
+            .s_ERROR { color: red }
16 65
         </style>
17 66
 
18 67
     </head>
@@ -25,16 +74,14 @@
25 74
         </div>
26 75
 
27 76
         <div class="events">
28
-            {% for P in session.phases %}
29
-                <div class="phase p_{{P.type}}">
30
-                    <span class="type">{{P.type}}</span> |
31
-                    <span class="name">{{P.name}}</span>
32
-                </div>
33
-                {% for E in P.all_events %}
34
-                    <div class="event e_{{E.etype}}">
35
-                        {{E.etype}} | {{ E.hint }} | {{ E.data }}
36
-                    </div>
37
-                {% endfor %}
77
+            {% for e in session.eventtree.children %}
78
+                {% if e.is_phase %}
79
+                    {{ phase(e) }}
80
+                {% elif e.is_message %}
81
+                    {{ message(e) }}
82
+                {% elif e.is_assert %}
83
+                    {{ assert(e) }}
84
+                {% endif %}
38 85
             {% endfor %}
39 86
         </div>
40 87
 

+ 119
- 126
src/python/jat.py View File

@@ -1,6 +1,7 @@
1 1
 #!/usr/bin/python2
2 2
 
3 3
 import yaml
4
+import sys
4 5
 
5 6
 
6 7
 class ParsingError(ValueError):
@@ -24,11 +25,11 @@ class _EventGroup(object):
24 25
 
25 26
     @property
26 27
     def fails(self):
27
-        return [e for e in self._events if e.etype == 'FAIL']
28
+        return [e for e in self._events if e.etype == 'ASSERT.FAIL']
28 29
 
29 30
     @property
30 31
     def passes(self):
31
-        return [e for e in self._events if e.etype == 'PASS']
32
+        return [e for e in self._events if e.etype == 'ASSERT.PASS']
32 33
 
33 34
     @property
34 35
     def events(self):
@@ -42,35 +43,108 @@ class _EventGroup(object):
42 43
         return lints
43 44
 
44 45
 
46
+class EventTree(object):
47
+
48
+    def __init__(self, events, head=None):
49
+        self.children = []
50
+        self.rest = [e for e in events]
51
+        self.head = head
52
+        while self.rest:
53
+            if self.peek().startswith('SESSION.'):
54
+                self.pop()
55
+            elif self.peek() == 'PHASE.START':
56
+                phead = self.pop()
57
+                pbody = self.pull_branch('PHASE.END')
58
+                self.children.append(Phase(pbody, phead))
59
+            elif self.peek().startswith('MESSAGE.'):
60
+                self.children.append(self.pop())
61
+            elif self.peek().startswith('ASSERT.'):
62
+                self.children.append(self.pop())
63
+
64
+    def peek(self):
65
+        return self.rest[0].etype
66
+
67
+    def pop(self):
68
+        return self.rest.pop(0)
69
+
70
+    def pull_branch(self, final_etype):
71
+        out = []
72
+        while self.rest:
73
+            if self.peek() == final_etype:
74
+                self.pop()
75
+                return out
76
+            else:
77
+                out.append(self.pop())
78
+        self.lints += [Lint('could not find: %s' % final_etype, out)]
79
+        return out
80
+
81
+    @property
82
+    def lints(self):
83
+        lints = []
84
+        for e in self.children:
85
+            lints += e.lints
86
+        return lints
87
+
88
+
89
+class Phase(EventTree):
90
+
91
+    is_phase = True
92
+
93
+    @property
94
+    def id(self):
95
+        return self.head._rawdata['phase']['id']
96
+
97
+    @property
98
+    def name(self):
99
+        return self.head._rawdata['phase']['name']
100
+
101
+    @property
102
+    def type(self):
103
+        return self.head._rawdata['phase']['type']
104
+
105
+    @property
106
+    def verdict(self):
107
+        out = 'UNKNOWN'
108
+        for e in self.children:
109
+            if e.is_assert:
110
+                if e.verdict == 'PASS':
111
+                    out = 'PASS'
112
+                elif e.verdict == 'FAIL':
113
+                    return 'FAIL'
114
+        return out
115
+
116
+
45 117
 class Event(object):
46 118
 
47 119
     is_assert = False
120
+    is_message = False
121
+    is_phase = False
48 122
 
49 123
     def __str__(self):
50 124
         return str(self._rawdata)
51 125
 
52 126
     def __init__(self, rawdata):
53 127
         self._rawdata = rawdata
54
-        self.origin = rawdata['origin']
55
-        self.stamp = rawdata['stamp']
56
-        self.hint = rawdata['hint']
57
-        self.beids = [BeId(b) for b in rawdata['beids']]
58
-        self.edata = rawdata['data']
59 128
         self.etype = rawdata['etype']
60
-        self.phaseid = rawdata['phase']['id']
61
-        self.phasename = rawdata['phase']['name']
62
-        self.phasetype = rawdata['phase']['type']
129
+        self.stamp = rawdata['stamp']
130
+        self.data = rawdata.get('data', dict())
63 131
 
64 132
     @staticmethod
65 133
     def from_data(data):
66 134
         etype = data['etype']
67
-        if etype in ['PASS', 'FAIL']:
135
+        if etype in ['ASSERT.PASS', 'ASSERT.FAIL']:
68 136
             cls = AssertEvent
69
-        elif etype == 'SINFO':
137
+        elif etype in ['PHASE.START', 'PHASE.END']:
70 138
             cls = StructureInfoEvent
71
-        elif etype == 'TEST_ERROR':
72
-            cls = TestErrorEvent
73
-        elif etype == 'PROMISE':
139
+        elif etype in ['SESSION.START', 'SESSION.RELOAD', 'SESSION.END']:
140
+            cls = StructureInfoEvent
141
+        elif etype == 'MESSAGE.INFO':
142
+            cls = MessageEvent
143
+        elif etype == 'MESSAGE.WARNING':
144
+            cls = MessageEvent
145
+        elif etype == 'MESSAGE.ERROR':
146
+            cls = MessageEvent
147
+        elif etype == 'MESSAGE.PROMISE':
74 148
             cls = PromiseEvent
75 149
         else:
76 150
             raise ParsingError("unknown event type: %s" % etype)
@@ -81,17 +155,33 @@ class Event(object):
81 155
         return []
82 156
 
83 157
 
84
-class StrayEvent(Event):
85
-    pass
158
+class MessageEvent(Event):
86 159
 
160
+    is_message = True
161
+
162
+    @property
163
+    def message(self):
164
+        return self._rawdata['message']
87 165
 
88
-class TestErrorEvent(Event):
166
+    @property
167
+    def severity(self):
168
+        return self.etype.split('.')[1]
89 169
 
170
+    @property
90 171
     def lints(self):
91
-        return [Lint("test error", self._rawdata)]
172
+        if self.severity == 'WARNING':
173
+            return [Lint("test warning", self._rawdata)]
174
+        elif self.severity == 'ERROR':
175
+            return [Lint("test error", self._rawdata)]
176
+        else:
177
+            return []
178
+
92 179
 
93 180
 class PromiseEvent(Event):
94
-    pass
181
+
182
+    @property
183
+    def lints(self):
184
+        return [Lint("promises are not supported (yet)", self._rawdata)]
95 185
 
96 186
 
97 187
 class StructureInfoEvent(Event):
@@ -103,28 +193,16 @@ class AssertEvent(Event):
103 193
     is_assert = True
104 194
 
105 195
     @property
106
-    def lints(self):
107
-        lints = []
108
-        if self.phaseid == '0000000000-000000000':
109
-            lints.append(Lint('assert outside phase', self))
110
-        return lints
111
-
112
-
113
-class BeId(object):
114
-
115
-    def __init__(self, data):
116
-        self._data = data
117
-
118
-
119
-class Phase(_EventGroup):
196
+    def caseid(self):
197
+        return self._rawdata['caseid']
120 198
 
121 199
     @property
122
-    def name(self):
123
-        return self.all_events[0].phasename
200
+    def hint(self):
201
+        return self._rawdata['hint']
124 202
 
125 203
     @property
126
-    def type(self):
127
-        return self.all_events[0].phasetype
204
+    def verdict(self):
205
+        return self.etype.split('.')[1]
128 206
 
129 207
 
130 208
 class EventLog(_EventGroup):
@@ -156,102 +234,17 @@ class Session(object):
156 234
             raise ParsingError('unsupported log format: %s' % self._format)
157 235
         self.jat_version = doc['jat_version']
158 236
         self.start = doc['start']
159
-        self.sessid = 'noid'
237
+        self.sessid = doc.get('sessid', 'noid')
160 238
         self.finalized = doc.get('finalized', False)
161 239
         self.end = doc.get('end')
162 240
         self.lints = []
163 241
         self.eventlog = EventLog.from_data(doc['events'])
164 242
         self.lints += self.eventlog.lints
165
-        self.phases, lints = self._es2ps(self.eventlog)
166
-        self.lints += lints
243
+        self.eventtree = EventTree(self.eventlog._events)
244
+        self.lints += self.eventtree.lints
167 245
         if not self.finalized:
168 246
             self.lints.append(Lint("session not finalized", self.sessid))
169 247
 
170
-    class _PhaseBuff:
171
-
172
-        def __init__(self):
173
-            self.phaseid = None
174
-            self.events = []
175
-
176
-        def is_anon(self):
177
-            return self.phaseid == '0000000000.000000000'
178
-
179
-        def dump2phase(self):
180
-            phase = Phase(self.events) 
181
-            self.phaseid = None
182
-            self.events = []
183
-            return phase
184
-
185
-        def match(self, event):
186
-            if self.phaseid is None:
187
-                return True
188
-            else:
189
-                return self.phaseid == event.phaseid
190
-
191
-        def append(self, event):
192
-            self.phaseid = event.phaseid
193
-            self.events.append(event)
194
-
195
-        def __nonzero__(self):
196
-            return bool(self.events)
197
-
198
-        def __bool__(self):
199
-            return bool(self.events)
200
-
201
-        def __len__(self):
202
-            return len(self.events)
203
-
204
-    @staticmethod
205
-    def _es2ps(eventlog):
206
-
207
-        def addlint(msg):
208
-            lints.append(Lint(msg, event))
209
-
210
-        phases = []
211
-        lints = []
212
-        this_phaseid = None
213
-        pbuff = Session._PhaseBuff()
214
-        for event in eventlog.all_events:
215
-            if event.origin == 'jat__sinit':
216
-                continue
217
-            if event.origin == 'jat__sfinish':
218
-                if pbuff:
219
-                    phases.append(pbuff.dump2phase())
220
-                    addlint('unfinished phase: %s' % pbuff.phaseid)
221
-                continue
222
-            if event.origin == 'jat__pend':
223
-                #
224
-                # Phase end
225
-                #
226
-                if pbuff:
227
-                    phases.append(pbuff.dump2phase())
228
-                else:
229
-                    addlint('dangling phase end')
230
-            elif event.origin == '__jat__pstart':
231
-                #
232
-                # Phase start
233
-                #
234
-                if pbuff:
235
-                    phases.append(pbuff.dump2phase())
236
-                    if not pbuff.is_anon():
237
-                        addlint('phase starts before previous ends')
238
-            elif pbuff:
239
-                #
240
-                # Second or later event
241
-                #
242
-                if not pbuff.match(event):
243
-                    phases.append(pbuff.dump2phase())
244
-                    addlint('phase mismatch: %s =! %s'\
245
-                            % (event.phaseid, pbuff.phaseid))
246
-                pbuff.append(event)
247
-            else:
248
-                #
249
-                # First event
250
-                #
251
-                pbuff.append(event)
252
-        return phases, lints
253
-
254
-
255 248
 def load(fpath):
256 249
     with open(fpath) as ylog:
257 250
         sessions = [Session(doc) for doc in yaml.load_all(ylog)]