1#     Copyright 2021, Kay Hayen, mailto:kay.hayen@gmail.com
2#
3#     Part of "Nuitka", an optimizing Python compiler that is compatible and
4#     integrates with CPython, but also works on its own.
5#
6#     Licensed under the Apache License, Version 2.0 (the "License");
7#     you may not use this file except in compliance with the License.
8#     You may obtain a copy of the License at
9#
10#        http://www.apache.org/licenses/LICENSE-2.0
11#
12#     Unless required by applicable law or agreed to in writing, software
13#     distributed under the License is distributed on an "AS IS" BASIS,
14#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15#     See the License for the specific language governing permissions and
16#     limitations under the License.
17#
18""" Search modes for Nuitka's test runner.
19
20The test runner can handle found errors, skip tests, etc. with search
21modes, which are implemented here.
22"""
23
24import hashlib
25import os
26import sys
27
28from nuitka.utils.FileOperations import areSamePaths
29
30
31class SearchModeBase(object):
32    def __init__(self):
33        self.may_fail = []
34
35    def consider(self, dirname, filename):
36        # Virtual method, pylint: disable=no-self-use,unused-argument
37        return True
38
39    def finish(self):
40        pass
41
42    def abortOnFinding(self, dirname, filename):
43        for candidate in self.may_fail:
44            if self._match(dirname, filename, candidate):
45                return False
46
47        return True
48
49    def getExtraFlags(self, dirname, filename):
50        # Virtual method, pylint: disable=no-self-use,unused-argument
51        return []
52
53    def mayFailFor(self, *names):
54        self.may_fail += names
55
56    @classmethod
57    def _match(cls, dirname, filename, candidate):
58        parts = [dirname, filename]
59
60        while None in parts:
61            parts.remove(None)
62        assert parts
63
64        path = os.path.join(*parts)
65
66        candidates = (
67            dirname,
68            filename,
69            filename.replace(".py", ""),
70            filename.split(".")[0],
71            path,
72            path.replace(".py", ""),
73        )
74
75        candidate2 = os.path.relpath(
76            candidate, os.path.dirname(sys.modules["__main__"].__file__)
77        )
78
79        return candidate.rstrip("/") in candidates or candidate2 in candidates
80
81    def exit(self, message):
82        # Virtual method, pylint: disable=no-self-use
83        sys.exit(message)
84
85    def isCoverage(self):
86        # Virtual method, pylint: disable=no-self-use
87        return False
88
89    def onErrorDetected(self, message):
90        self.exit(message)
91
92
93class SearchModeImmediate(SearchModeBase):
94    pass
95
96
97class SearchModeByPattern(SearchModeBase):
98    def __init__(self, start_at):
99        SearchModeBase.__init__(self)
100
101        self.active = False
102        self.start_at = start_at
103
104    def consider(self, dirname, filename):
105        if self.active:
106            return True
107
108        self.active = self._match(dirname, filename, self.start_at)
109        return self.active
110
111    def finish(self):
112        if not self.active:
113            sys.exit("Error, became never active.")
114
115
116class SearchModeResume(SearchModeBase):
117    def __init__(self, tests_path):
118        SearchModeBase.__init__(self)
119
120        tests_path = os.path.normcase(os.path.abspath(tests_path))
121        version = sys.version
122
123        if str is not bytes:
124            tests_path = tests_path.encode("utf8")
125            version = version.encode("utf8")
126
127        case_hash = hashlib.md5(tests_path)
128        case_hash.update(version)
129
130        from .Common import getTestingCacheDir
131
132        cache_filename = os.path.join(getTestingCacheDir(), case_hash.hexdigest())
133
134        self.cache_filename = cache_filename
135
136        if os.path.exists(cache_filename):
137            with open(cache_filename, "r") as f:
138                self.resume_from = f.read() or None
139        else:
140            self.resume_from = None
141
142        self.active = not self.resume_from
143
144    def consider(self, dirname, filename):
145        parts = [dirname, filename]
146
147        while None in parts:
148            parts.remove(None)
149        assert parts
150
151        path = os.path.join(*parts)
152
153        if self.active:
154            with open(self.cache_filename, "w") as f:
155                f.write(path)
156
157            return True
158
159        if areSamePaths(path, self.resume_from):
160            self.active = True
161
162        return self.active
163
164    def finish(self):
165        os.unlink(self.cache_filename)
166        if not self.active:
167            sys.exit("Error, became never active, restarting next time.")
168
169
170class SearchModeCoverage(SearchModeBase):
171    def getExtraFlags(self, dirname, filename):
172        return ["coverage"]
173
174    def isCoverage(self):
175        return True
176
177
178class SearchModeOnly(SearchModeByPattern):
179    def __init__(self, start_at):
180        SearchModeByPattern.__init__(self, start_at=start_at)
181
182        self.done = False
183
184    def consider(self, dirname, filename):
185        if self.done:
186            return False
187        else:
188            active = SearchModeByPattern.consider(
189                self, dirname=dirname, filename=filename
190            )
191
192            if active:
193                self.done = True
194
195            return active
196
197
198class SearchModeAll(SearchModeBase):
199    def __init__(self):
200        SearchModeBase.__init__(self)
201        self.total_errors = 0
202
203    def updateTotalErrors(self):
204        self.total_errors += 1
205
206    def onErrorDetected(self, message):
207        self.updateTotalErrors()
208
209    def finish(self):
210        self.exit("Total " + str(self.total_errors) + " error(s) found.")
211