source: trunk/autoquest-generic-event-monitor-test/src/test/java/de/ugoe/cs/autoquest/genericeventmonitor/GenericEventMonitorTest.java @ 2286

Last change on this file since 2286 was 2286, checked in by pharms, 4 years ago

added a test for checking correct log file synchronization

  • Property svn:mime-type set to text/plain
File size: 25.3 KB
Line 
1//   Copyright 2012 Georg-August-Universität Göttingen, Germany
2//
3//   Licensed under the Apache License, Version 2.0 (the "License");
4//   you may not use this file except in compliance with the License.
5//   You may obtain a copy of the License at
6//
7//       http://www.apache.org/licenses/LICENSE-2.0
8//
9//   Unless required by applicable law or agreed to in writing, software
10//   distributed under the License is distributed on an "AS IS" BASIS,
11//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//   See the License for the specific language governing permissions and
13//   limitations under the License.
14
15package de.ugoe.cs.autoquest.genericeventmonitor;
16
17import static org.junit.Assert.*;
18
19import java.lang.String;
20
21import java.io.File;
22import java.util.ArrayList;
23import java.util.Collection;
24import java.util.HashMap;
25import java.util.HashSet;
26import java.util.Iterator;
27import java.util.List;
28import java.util.Map;
29
30import org.apache.http.HttpEntity;
31import org.apache.http.HttpResponse;
32import org.apache.http.client.methods.HttpPost;
33import org.apache.http.entity.ContentType;
34import org.apache.http.entity.StringEntity;
35import org.apache.http.impl.client.DefaultHttpClient;
36import org.junit.After;
37import org.junit.Before;
38import org.junit.Test;
39
40import de.ugoe.cs.autoquest.eventcore.Event;
41import de.ugoe.cs.autoquest.eventcore.HierarchicalEventTargetModel;
42import de.ugoe.cs.autoquest.eventcore.IEventTarget;
43import de.ugoe.cs.autoquest.eventcore.StringEventType;
44import de.ugoe.cs.autoquest.genericeventmonitor.GenericEventMonitor;
45import de.ugoe.cs.autoquest.plugin.genericevents.GenericEventLogParser;
46import de.ugoe.cs.autoquest.plugin.genericevents.eventCore.GenericEventTarget;
47import de.ugoe.cs.util.console.TextConsole;
48
49/**
50 * 
51 * @author Patrick Harms
52 */
53public class GenericEventMonitorTest {
54
55    /**
56     *
57     */
58    public static final TextConsole CONSOLE = new TextConsole();
59   
60    /**
61     *
62     */
63    private final static String LOG_FILE_DIR = "target/tmp/logfiles/";
64   
65    /**
66     *
67     */
68    private static final int PORT = 19098;
69
70    /**
71     *
72     */
73    private GenericEventMonitor genericEventMonitor;
74
75    /**
76     *
77     */
78    @Before
79    public void setUp() throws Exception {
80        genericEventMonitor = new GenericEventMonitor(new String[] { LOG_FILE_DIR, Integer.toString(PORT) });
81        genericEventMonitor.init();
82        genericEventMonitor.start();
83    }
84
85    /**
86     *
87     */
88    @After
89    public void tearDown() throws Exception {
90        if (genericEventMonitor != null) {
91            try {
92                genericEventMonitor.stop();
93            }
94            finally {
95                genericEventMonitor = null;
96            }
97        }
98       
99        deleteFiles(new File(LOG_FILE_DIR));
100    }
101
102    /**
103     *
104     */
105    @Test
106    public void testOneSimpleMessage() throws Exception {
107        String clientId = "123";
108        String appId = "456";
109       
110        String message =
111            "{" +
112            "  \"message\": {" +
113            "    \"clientInfos\": {" +
114            "      \"clientId\":\"" + clientId + "\"," +
115            "      \"appId\":\"" + appId + "\"," +
116            "    }," +
117            "    \"targetStructure\": [{" +
118            "      \"targetId\":\"target1\"," +
119            "      \"param1\":\"value1\"," +
120            "      \"param3\":\"value3\"," +
121            "      \"param2\":\"value2\"," +
122            "      \"children\":" +
123            "      [ {" +
124            "          \"targetId\":\"target3\"," +
125            "          \"index\":\"0\"," +
126            "        }," +
127            "        {" +
128            "          \"targetId\":\"target2\"," +
129            "          \"htmlId\":\"gsr\"," +
130            "        }" +
131            "      ]" +
132            "    }]," +
133            "    \"events\":" +
134            "    [ {" +
135            "        \"time\":\"12345\"," +
136            "        \"targetId\":\"target2\"," +
137            "        \"type\":\"gaze\"," +
138            "        \"xcoordinate\": \"194\"," +
139            "        \"ycoordinate\": \"12\"" +
140            "      }" +
141            "    ]" +
142            "  }" +
143            "}";
144
145        sendMessageAndAssertResponse(message);
146       
147        genericEventMonitor.stop();
148        genericEventMonitor = null;
149
150        File logFile = new File(LOG_FILE_DIR + File.separator + appId + File.separator +
151                                clientId + File.separator + "genericevents_" + clientId + "_000.log");
152       
153        assertTrue(logFile.exists());
154       
155        GenericEventLogParser parser = new GenericEventLogParser(new HashSet<String>());
156       
157        parser.parseFile(logFile);
158       
159        // check the GUI model
160        HierarchicalEventTargetModel<GenericEventTarget> guiModel = parser.getHierarchicalEventTargetModel();
161        assertNotNull(guiModel);
162       
163        List<GenericEventTarget> nodes = guiModel.getRootElements();
164        assertNotNull(nodes);
165        assertEquals(1, nodes.size());
166       
167        // get root node
168        GenericEventTarget node = nodes.get(0);
169        assertNotNull(node);
170        assertEquals("target1", node.getStringIdentifier());
171        assertEquals("Generic Event", node.getPlatform());
172       
173        nodes = guiModel.getChildren(node);
174        assertNotNull(nodes);
175        assertEquals(2, nodes.size());
176       
177        // get children node
178        node = nodes.get(0);
179        assertNotNull(node);
180        assertTrue("target2".equals(node.getStringIdentifier()) || "target3".equals(node.getStringIdentifier()));
181        assertEquals("Generic Event", node.getPlatform());
182       
183        node = nodes.get(1);
184        assertNotNull(node);
185        assertTrue("target2".equals(node.getStringIdentifier()) || "target3".equals(node.getStringIdentifier()));
186        assertEquals("Generic Event", node.getPlatform());
187       
188        Collection<List<Event>> sequences = parser.getSequences();
189       
190        assertNotNull(sequences);
191       
192        Iterator<List<Event>> iterator = sequences.iterator();
193        assertTrue(iterator.hasNext());
194       
195        List<Event> sequence = iterator.next();
196        assertFalse(iterator.hasNext());
197       
198        assertNotNull(sequence);
199        assertEquals(1, sequence.size());
200       
201        IEventTarget target = null;
202        for (IEventTarget candidate : nodes) {
203            if ("target2".equals(candidate.getStringIdentifier())) {
204                target = candidate;
205                break;
206            }
207        }
208       
209        assertEvent(sequence.get(0), "gaze", target);
210       
211    }
212
213    /**
214     *
215     */
216    @Test
217    public void testSeveralMessagesInOneSession() throws Exception {
218        String clientId = "123";
219        String appId = "456";
220       
221        String message =
222            "{" +
223            "  \"message\": {" +
224            "    \"clientInfos\": {" +
225            "      \"clientId\":\"" + clientId + "\"," +
226            "      \"appId\":\"" + appId + "\"," +
227            "    }," +
228            "    \"targetStructure\": [{" +
229            "      \"targetId\":\"html\"," +
230            "      \"index\":\"0\"," +
231            "      \"children\":" +
232            "      [ {" +
233            "          \"tagName\":\"body\"," +
234            "          \"targetId\":\"gsr\"," +
235            "          \"children\":" +
236            "          [ {" +
237            "              \"tagName\":\"input_button\"," +
238            "              \"targetId\":\"input1\"," +
239            "            }," +
240            "            {" +
241            "              \"tagName\":\"input_button\"," +
242            "              \"targetId\":\"input2\"," +
243            "            }," +
244            "            {" +
245            "              \"tagName\":\"input_button\"," +
246            "              \"targetId\":\"input3\"," +
247            "            }," +
248            "            {" +
249            "              \"tagName\":\"input_button\"," +
250            "              \"targetId\":\"input4\"," +
251            "            }," +
252            "            {" +
253            "              \"tagName\":\"input_button\"," +
254            "              \"targetId\":\"input5\"," +
255            "            }," +
256            "            {" +
257            "              \"tagName\":\"input_button\"," +
258            "              \"targetId\":\"input6\"," +
259            "            }," +
260            "            {" +
261            "              \"tagName\":\"input_button\"," +
262            "              \"targetId\":\"input7\"," +
263            "            }," +
264            "            {" +
265            "              \"tagName\":\"input_button\"," +
266            "              \"targetId\":\"input8\"," +
267            "            }," +
268            "            {" +
269            "              \"tagName\":\"input_button\"," +
270            "              \"targetId\":\"input9\"," +
271            "            }," +
272            "            {" +
273            "              \"tagName\":\"input_button\"," +
274            "              \"targetId\":\"input10\"," +
275            "            }," +
276            "          ]" +
277            "        }" +
278            "      ]" +
279            "    }]," +
280            "    \"events\":" +
281            "    [ {" +
282            "        \"time\":\"1\"," +
283            "        \"targetId\":\"input1\"," +
284            "        \"type\":\"onclick\"," +
285            "        \"coordinates\": \"194\"" +
286            "      }," +
287            "      {" +
288            "        \"time\":\"2\"," +
289            "        \"targetId\":\"input2\"," +
290            "        \"type\":\"ondblclick\"," +
291            "        \"coordinates\": \"194\"" +
292            "      }," +
293            "      {" +
294            "        \"time\":\"3\"," +
295            "        \"targetId\":\"input3\"," +
296            "        \"type\":\"onfocus\"" +
297            "      }," +
298            "      {" +
299            "        \"time\":\"4\"," +
300            "        \"targetId\":\"input4\"," +
301            "        \"type\":\"onclick\"," +
302            "        \"coordinates\": \"255\"" +
303            "      }," +
304            "      {" +
305            "        \"time\":\"5\"," +
306            "        \"targetId\":\"input5\"," +
307            "        \"type\":\"onfocus\"" +
308            "      }," +
309            "      {" +
310            "        \"time\":\"6\"," +
311            "        \"targetId\":\"input6\"," +
312            "        \"type\":\"onfocus\"" +
313            "      }," +
314            "      {" +
315            "        \"time\":\"7\"," +
316            "        \"targetId\":\"input7\"," +
317            "        \"type\":\"onfocus\"" +
318            "      }," +
319            "      {" +
320            "        \"time\":\"8\"," +
321            "        \"targetId\":\"input8\"," +
322            "        \"type\":\"onclick\"," +
323            "        \"coordinates\": \"255\"" +
324            "      }," +
325            "      {" +
326            "        \"time\":\"9\"," +
327            "        \"targetId\":\"input9\"," +
328            "        \"type\":\"onscroll\"," +
329            "        \"scrollPosition\": \"194\"" +
330            "      }," +
331            "      {" +
332            "        \"time\":\"10\"," +
333            "        \"targetId\":\"input10\"," +
334            "        \"type\":\"onclick\"," +
335            "        \"coordinates\": \"194\"" +
336            "      }" +
337            "    ]" +
338            "  }" +
339            "}";
340 
341        sendMessageAndAssertResponse(message);
342       
343        genericEventMonitor.stop();
344        genericEventMonitor = null;
345
346        File logFile = new File(LOG_FILE_DIR + File.separator + appId + File.separator +
347                                clientId + File.separator + "genericevents_" + clientId + "_000.log");
348       
349        assertTrue(logFile.exists());
350       
351       
352        GenericEventLogParser parser = new GenericEventLogParser(new HashSet<String>());
353       
354        parser.parseFile(logFile);
355       
356        // check the GUI model
357        HierarchicalEventTargetModel<GenericEventTarget> guiModel = parser.getHierarchicalEventTargetModel();
358        assertNotNull(guiModel);
359       
360        List<GenericEventTarget> nodes = guiModel.getRootElements();
361        assertNotNull(nodes);
362        assertEquals(1, nodes.size());
363       
364        // get root node
365        GenericEventTarget node = nodes.get(0);
366        assertNotNull(node);
367        assertEquals("html", node.getStringIdentifier());
368        assertEquals("Generic Event", node.getPlatform());
369       
370        nodes = guiModel.getChildren(node);
371        assertNotNull(nodes);
372        assertEquals(1, nodes.size());
373       
374        // get root child node
375        GenericEventTarget body = nodes.get(0);
376        assertNotNull(body);
377        assertEquals("gsr", body.getStringIdentifier());
378        assertEquals("Generic Event", body.getPlatform());
379       
380        nodes = guiModel.getChildren(body);
381        assertNotNull(nodes);
382        assertEquals(10, nodes.size());
383       
384        // get children node
385        Map<String, GenericEventTarget> inputs = new HashMap<>();
386       
387        for (GenericEventTarget candidate: nodes) {
388            assertNotNull(candidate);
389            assertTrue(candidate.getStringIdentifier().startsWith("input"));
390            assertEquals("Generic Event", candidate.getPlatform());
391           
392            inputs.put(candidate.getStringIdentifier(), candidate);
393
394            assertNotNull(guiModel.getChildren(candidate));
395            assertEquals(0, guiModel.getChildren(candidate).size());
396        }
397       
398       
399        // check the sequences
400        Collection<List<Event>> sequences = parser.getSequences();
401       
402        assertNotNull(sequences);
403       
404        Iterator<List<Event>> iterator = sequences.iterator();
405        assertTrue(iterator.hasNext());
406       
407        List<Event> sequence = iterator.next();
408        assertFalse(iterator.hasNext());
409       
410        assertNotNull(sequence);
411        assertEquals(10, sequence.size());
412       
413        assertEvent(sequence.get(0), "onclick", inputs.get("input1"));
414        assertEvent(sequence.get(1), "ondblclick", inputs.get("input2"));
415        assertEvent(sequence.get(2), "onfocus", inputs.get("input3"));
416        assertEvent(sequence.get(3), "onclick", inputs.get("input4"));
417        assertEvent(sequence.get(4), "onfocus", inputs.get("input5"));
418        assertEvent(sequence.get(5), "onfocus", inputs.get("input6"));
419        assertEvent(sequence.get(6), "onfocus", inputs.get("input7"));
420        assertEvent(sequence.get(7), "onclick", inputs.get("input8"));
421        assertEvent(sequence.get(8), "onscroll", inputs.get("input9"));
422        assertEvent(sequence.get(9), "onclick", inputs.get("input10"));
423
424    }
425
426    /**
427     *
428     */
429    @Test
430    public void testSeveralSessions() throws Exception {
431        String clientId = "123";
432        String appId = "456";
433       
434        String message =
435            "{" +
436            "  \"message\": {" +
437            "    \"clientInfos\": {" +
438            "      \"clientId\":\"" + clientId + "\"," +
439            "      \"appId\":\"" + appId + "\"," +
440            "    }," +
441            "    \"targetStructure\": [{" +
442            "      \"targetId\":\"target1\"," +
443            "      \"param1\":\"value1\"," +
444            "      \"param3\":\"value3\"," +
445            "      \"param2\":\"value2\"," +
446            "      \"children\":" +
447            "      [ {" +
448            "          \"targetId\":\"target3\"," +
449            "          \"index\":\"0\"," +
450            "        }," +
451            "        {" +
452            "          \"targetId\":\"target2\"," +
453            "          \"htmlId\":\"gsr\"," +
454            "        }" +
455            "      ]" +
456            "    }]," +
457            "    \"events\":" +
458            "    [ {" +
459            "        \"time\":\"12345\"," +
460            "        \"targetId\":\"target2\"," +
461            "        \"type\":\"gaze\"," +
462            "        \"xcoordinate\": \"194\"," +
463            "        \"ycoordinate\": \"12\"" +
464            "      }" +
465            "    ]" +
466            "  }" +
467            "}";
468
469        sendMessageAndAssertResponse(message);
470       
471        int numberOfSessions = 10;
472        for (int i = 0; i < numberOfSessions; i++) {
473            genericEventMonitor.stop();
474            genericEventMonitor =
475                new GenericEventMonitor(new String[] { LOG_FILE_DIR, Integer.toString(PORT) });
476           
477            genericEventMonitor.init();
478            genericEventMonitor.start();
479            sendMessageAndAssertResponse(message);
480        }
481       
482        genericEventMonitor.stop();
483        genericEventMonitor = null;
484       
485        GenericEventLogParser parser = new GenericEventLogParser(new HashSet<String>());
486       
487        // assert 9 already rotated log files
488        for (int i = 0; i < numberOfSessions; i++) {
489            File logFile = new File(LOG_FILE_DIR + File.separator + appId + File.separator +
490                                    clientId + File.separator + "genericevents_" + clientId + "_00" +
491                                    i + ".log");
492       
493            assertTrue(logFile.exists());
494       
495            parser.parseFile(logFile);
496        }
497
498        // check the GUI model
499        HierarchicalEventTargetModel<GenericEventTarget> guiModel = parser.getHierarchicalEventTargetModel();
500        assertNotNull(guiModel);
501       
502        List<GenericEventTarget> nodes = guiModel.getRootElements();
503        assertNotNull(nodes);
504        assertEquals(1, nodes.size());
505       
506        // get root node
507        GenericEventTarget node = nodes.get(0);
508        assertNotNull(node);
509        assertEquals("target1", node.getStringIdentifier());
510        assertEquals("Generic Event", node.getPlatform());
511       
512        nodes = guiModel.getChildren(node);
513        assertNotNull(nodes);
514        assertEquals(2, nodes.size());
515       
516        // get children node
517        node = nodes.get(0);
518        assertNotNull(node);
519        assertTrue("target2".equals(node.getStringIdentifier()) || "target3".equals(node.getStringIdentifier()));
520        assertEquals("Generic Event", node.getPlatform());
521       
522        node = nodes.get(1);
523        assertNotNull(node);
524        assertTrue("target2".equals(node.getStringIdentifier()) || "target3".equals(node.getStringIdentifier()));
525        assertEquals("Generic Event", node.getPlatform());
526       
527       
528        // check the sequences
529        Collection<List<Event>> sequences = parser.getSequences();
530       
531        assertNotNull(sequences);
532        assertEquals(numberOfSessions, sequences.size());
533       
534        Iterator<List<Event>> iterator = sequences.iterator();
535       
536        IEventTarget target = null;
537        for (IEventTarget candidate : nodes) {
538            if ("target2".equals(candidate.getStringIdentifier())) {
539                target = candidate;
540                break;
541            }
542        }
543       
544        while (iterator.hasNext()) {
545            List<Event> sequence = iterator.next();
546       
547            assertNotNull(sequence);
548            assertEquals(1, sequence.size());
549           
550            assertEvent(sequence.get(0), "gaze", target);
551        }
552       
553    }
554
555    /**
556     *
557     */
558    @Test
559    public void testManyRequestsInOneSession() throws Exception {
560        int noOfMessages = 100;
561        int noOfEventsPerMessage = 1000;
562        String clientId = "123";
563        String appId = "456";
564       
565        List<String> messages = new ArrayList<>();
566       
567        int eventId = 0;
568       
569        for (int i = 0; i < noOfMessages; i++) {
570            StringBuffer message = new StringBuffer(
571                "{" +
572                "  \"message\": {" +
573                "    \"clientInfos\": {" +
574                "      \"clientId\":\"" + clientId + "\"," +
575                "      \"appId\":\"" + appId + "\"," +
576                "    }," +
577                "    \"targetStructure\": [{" +
578                "      \"targetId\":\"target1\"," +
579                "      \"children\":" +
580                "      [ {" +
581                "          \"targetId\":\"target2\"," +
582                "        }" +
583                "      ]" +
584                "    }]," +
585                "    \"events\":" +
586                "    [");
587           
588            for (int j = 0; j < noOfEventsPerMessage; j++) {
589                message.append(
590                    "      {" +
591                    "        \"time\":\"" + eventId++ + "\"," +
592                    "        \"targetId\":\"target2\"," +
593                    "        \"type\":\"gaze\"," +
594                    "      },");
595            }
596
597            message.append(
598                "    ]" +
599                "  }" +
600                "}");
601           
602            messages.add(message.toString());
603        }
604       
605        List<Thread> threads = new ArrayList<>();
606       
607        for (String message : messages) {
608            threads.add(sendMessage(message));
609        }
610       
611        for (Thread thread : threads) {
612            thread.join();
613        }
614       
615        genericEventMonitor.stop();
616        genericEventMonitor = null;
617       
618        GenericEventLogParser parser = new GenericEventLogParser(new HashSet<String>());
619       
620        File logFile = new File(LOG_FILE_DIR + File.separator + appId + File.separator +
621                                clientId + File.separator + "genericevents_" + clientId + "_000.log");
622       
623        assertTrue(logFile.exists());
624       
625        parser.parseFile(logFile);
626
627        // check the GUI model
628        HierarchicalEventTargetModel<GenericEventTarget> guiModel = parser.getHierarchicalEventTargetModel();
629        assertNotNull(guiModel);
630       
631        List<GenericEventTarget> nodes = guiModel.getRootElements();
632        assertNotNull(nodes);
633        assertEquals(1, nodes.size());
634       
635        // get root node
636        GenericEventTarget node = nodes.get(0);
637        assertNotNull(node);
638        assertEquals("target1", node.getStringIdentifier());
639        assertEquals("Generic Event", node.getPlatform());
640       
641        nodes = guiModel.getChildren(node);
642        assertNotNull(nodes);
643        assertEquals(1, nodes.size());
644       
645        // get children node
646        GenericEventTarget target = nodes.get(0);
647        assertNotNull(target);
648        assertEquals("target2", target.getStringIdentifier());
649        assertEquals("Generic Event", target.getPlatform());
650       
651       
652        // check the sequences
653        Collection<List<Event>> sequences = parser.getSequences();
654       
655        assertNotNull(sequences);
656        assertEquals(1, sequences.size());
657       
658        Iterator<List<Event>> iterator = sequences.iterator();
659       
660        while (iterator.hasNext()) {
661            List<Event> sequence = iterator.next();
662       
663            assertNotNull(sequence);
664            assertEquals(noOfMessages * noOfEventsPerMessage, sequence.size());
665           
666            for (Event event : sequence) {
667              assertEvent(event, "gaze", target);
668            }
669        }
670       
671    }
672
673    /**
674     *
675     */
676    private void sendMessageAndAssertResponse(String message) throws Exception {
677        DefaultHttpClient httpclient = new DefaultHttpClient();
678        //HttpPost httpPost = new HttpPost("https://swe-tooling.informatik.uni-goettingen.de/autoquest-genericmonitor/");
679        HttpPost httpPost = new HttpPost("http://localhost:" + PORT + "/");
680        HttpEntity entity = new StringEntity(message, ContentType.APPLICATION_JSON);
681        httpPost.setEntity(entity);
682       
683        try {
684            HttpResponse response = httpclient.execute(httpPost);
685           
686            // the monitor always returns 200 without any additional information. The client must
687            // never get more or less information. This is especially important for preventing
688            // hackers from finding out more
689            assertEquals(200, response.getStatusLine().getStatusCode());
690            assertTrue
691                ((response.getEntity() == null) || (response.getEntity().getContentLength() == 0) ||
692                 ((response.getEntity().getContentLength() == 1) &&
693                  (response.getEntity().getContent().read() == ' ')));
694        }
695        finally {
696            httpPost.releaseConnection();
697        }
698    }
699
700    /**
701     *
702     */
703    private Thread sendMessage(final String message) throws Exception {
704        Thread thread = new Thread(new Runnable() {
705            @Override
706            public void run() {
707                try {
708                    System.out.println("sending data");
709                    sendMessageAndAssertResponse(message);
710                    System.out.println("data send");
711                }
712                catch (Exception e) {
713                    e.printStackTrace();
714                }
715            }
716        });
717       
718        thread.start();
719       
720        return thread;
721    }
722
723   
724    /**
725     *
726     */
727    private void assertEvent(Event event, String name, IEventTarget target) {
728        assertEquals(StringEventType.class, event.getType().getClass());
729        assertEquals(name, ((StringEventType) event.getType()).toString());
730        assertEquals(target, event.getTarget());
731    }
732
733    /**
734     *
735     */
736    private void deleteFiles(File file) {
737        if (file.exists()) {
738            if (file.isDirectory()) {
739                File[] files = file.listFiles();
740                if (files != null) {
741                    for (File child : files) {
742                       deleteFiles(child);
743                    }
744                }
745            }
746           
747            try {
748                file.delete();
749            }
750            catch (Exception e) {
751                // ignore and delete as much as possible
752            }
753        }
754    }
755
756}
Note: See TracBrowser for help on using the repository browser.