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
 Requires: %shellfu_req
24
 Requires: %shellfu_req
25
 Requires: %saturnin_req
25
 Requires: %saturnin_req
26
 Requires: shellfu-bash
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
 Requires: shellfu-bash-pretty
29
 Requires: shellfu-bash-pretty
30
 Summary: JAT harness and test discovery
30
 Summary: JAT harness and test discovery
31
 %description minimal
31
 %description minimal

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

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
 <!doctype html>
34
 <!doctype html>
2
 <html>
35
 <html>
3
     <head>
36
     <head>
4
         <title>JAT report {{ session.sessid }}</title>
37
         <title>JAT report {{ session.sessid }}</title>
5
 
38
 
6
         <style type="text/css">
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
             .p_test { color: yellow }
54
             .p_test { color: yellow }
12
             .p_diag { color: lightblue }
55
             .p_diag { color: lightblue }
13
             .p_cleanup { color: blue }
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
         </style>
65
         </style>
17
 
66
 
18
     </head>
67
     </head>
25
         </div>
74
         </div>
26
 
75
 
27
         <div class="events">
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
             {% endfor %}
85
             {% endfor %}
39
         </div>
86
         </div>
40
 
87
 

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

1
 #!/usr/bin/python2
1
 #!/usr/bin/python2
2
 
2
 
3
 import yaml
3
 import yaml
4
+import sys
4
 
5
 
5
 
6
 
6
 class ParsingError(ValueError):
7
 class ParsingError(ValueError):
24
 
25
 
25
     @property
26
     @property
26
     def fails(self):
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
     @property
30
     @property
30
     def passes(self):
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
     @property
34
     @property
34
     def events(self):
35
     def events(self):
42
         return lints
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
 class Event(object):
117
 class Event(object):
46
 
118
 
47
     is_assert = False
119
     is_assert = False
120
+    is_message = False
121
+    is_phase = False
48
 
122
 
49
     def __str__(self):
123
     def __str__(self):
50
         return str(self._rawdata)
124
         return str(self._rawdata)
51
 
125
 
52
     def __init__(self, rawdata):
126
     def __init__(self, rawdata):
53
         self._rawdata = rawdata
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
         self.etype = rawdata['etype']
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
     @staticmethod
132
     @staticmethod
65
     def from_data(data):
133
     def from_data(data):
66
         etype = data['etype']
134
         etype = data['etype']
67
-        if etype in ['PASS', 'FAIL']:
135
+        if etype in ['ASSERT.PASS', 'ASSERT.FAIL']:
68
             cls = AssertEvent
136
             cls = AssertEvent
69
-        elif etype == 'SINFO':
137
+        elif etype in ['PHASE.START', 'PHASE.END']:
70
             cls = StructureInfoEvent
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
             cls = PromiseEvent
148
             cls = PromiseEvent
75
         else:
149
         else:
76
             raise ParsingError("unknown event type: %s" % etype)
150
             raise ParsingError("unknown event type: %s" % etype)
81
         return []
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
     def lints(self):
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
 class PromiseEvent(Event):
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
 class StructureInfoEvent(Event):
187
 class StructureInfoEvent(Event):
103
     is_assert = True
193
     is_assert = True
104
 
194
 
105
     @property
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
     @property
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
     @property
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
 class EventLog(_EventGroup):
208
 class EventLog(_EventGroup):
156
             raise ParsingError('unsupported log format: %s' % self._format)
234
             raise ParsingError('unsupported log format: %s' % self._format)
157
         self.jat_version = doc['jat_version']
235
         self.jat_version = doc['jat_version']
158
         self.start = doc['start']
236
         self.start = doc['start']
159
-        self.sessid = 'noid'
237
+        self.sessid = doc.get('sessid', 'noid')
160
         self.finalized = doc.get('finalized', False)
238
         self.finalized = doc.get('finalized', False)
161
         self.end = doc.get('end')
239
         self.end = doc.get('end')
162
         self.lints = []
240
         self.lints = []
163
         self.eventlog = EventLog.from_data(doc['events'])
241
         self.eventlog = EventLog.from_data(doc['events'])
164
         self.lints += self.eventlog.lints
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
         if not self.finalized:
245
         if not self.finalized:
168
             self.lints.append(Lint("session not finalized", self.sessid))
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
 def load(fpath):
248
 def load(fpath):
256
     with open(fpath) as ylog:
249
     with open(fpath) as ylog:
257
         sessions = [Session(doc) for doc in yaml.load_all(ylog)]
250
         sessions = [Session(doc) for doc in yaml.load_all(ylog)]