Failed Conditions
Pull Request — master (#1170)
by Mischa
07:00 queued 04:25
created

coalib/processes/Processing.py (1 issue)

Severity
1
import multiprocessing
2
import queue
3
import os
4
import platform
5
import subprocess
6
7
from coalib.collecting.Collectors import collect_files
8
from coalib.collecting import Dependencies
9
from coalib.misc.StringConverter import StringConverter
10
from coalib.output.printers import LOG_LEVEL
11
from coalib.processes.BearRunning import run
12
from coalib.processes.CONTROL_ELEMENT import CONTROL_ELEMENT
13
from coalib.results.Result import Result
14
from coalib.results.RESULT_SEVERITY import RESULT_SEVERITY
15
from coalib.results.SourceRange import SourceRange
16
from coalib.settings.Setting import path_list
17
from coalib.processes.LogPrinterThread import LogPrinterThread
18
from coalib.results.result_actions.OpenEditorAction import OpenEditorAction
0 ignored issues
show
Unused OpenEditorAction imported from coalib.results.result_actions.OpenEditorAction
Loading history...
19
from coalib.results.result_actions.ApplyPatchAction import ApplyPatchAction
20
from coalib.results.result_actions.PrintDebugMessageAction import (
21
    PrintDebugMessageAction)
22
from coalib.results.result_actions.ShowPatchAction import ShowPatchAction
23
24
25
ACTIONS = [ApplyPatchAction,
26
           PrintDebugMessageAction,
27
           ShowPatchAction]
28
29
30
def get_cpu_count():
31
    try:
32
        return multiprocessing.cpu_count()
33
    # cpu_count is not implemented for some CPU architectures/OSes
34
    except NotImplementedError:  # pragma: no cover
35
        return 2
36
37
38
def fill_queue(queue_fill, any_list):
39
    """
40
    Takes element from a list and populates a queue with those elements.
41
42
    :param queue_fill: The queue to be filled.
43
    :param any_list:   List containing the elements.
44
    """
45
    for elem in any_list:
46
        queue_fill.put(elem)
47
48
49
def get_running_processes(processes):
50
    return sum((1 if process.is_alive() else 0) for process in processes)
51
52
53
def create_process_group(command_array, **kwargs):
54
    if platform.system() == "Windows":  # pragma: no cover
55
        proc = subprocess.Popen(
56
            command_array,
57
            creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
58
            **kwargs)
59
    else:
60
        proc = subprocess.Popen(command_array,
61
                                preexec_fn=os.setsid,
62
                                **kwargs)
63
    return proc
64
65
66
def get_default_actions(section):
67
    """
68
    Parses the key `default_actions` in the given section.
69
70
    :param section:    The section where to parse from.
71
    :return:           A dict with the bearname as keys and their default
72
                       actions as values and another dict that contains bears
73
                       and invalid action names.
74
    """
75
    try:
76
        default_actions = dict(section["default_actions"])
77
    except IndexError:
78
        return {}, {}
79
80
    action_dict = {action.get_metadata().name: action for action in ACTIONS}
81
    invalid_action_set = default_actions.values() - action_dict.keys()
82
    invalid_actions = {}
83
    if len(invalid_action_set) != 0:
84
        invalid_actions = {
85
            bear: action
86
            for bear, action in default_actions.items()
87
            if action in invalid_action_set}
88
        for invalid in invalid_actions.keys():
89
            del default_actions[invalid]
90
91
    actions = {bearname: action_dict[action_name]
92
               for bearname, action_name in default_actions.items()}
93
    return actions, invalid_actions
94
95
96
def autoapply_actions(results,
97
                      file_dict,
98
                      file_diff_dict,
99
                      section,
100
                      log_printer):
101
    """
102
    Auto-applies actions like defined in the given section.
103
104
    :param results:        A list of results.
105
    :param file_dict:      A dictionary containing the name of files and its
106
                           contents.
107
    :param file_diff_dict: A dictionary that contains filenames as keys and
108
                           diff objects as values.
109
    :param section:        The section.
110
    :param log_printer:    A log printer instance to log messages on.
111
    :return:               A list of unprocessed results.
112
    """
113
114
    default_actions, invalid_actions = get_default_actions(section)
115
116
    for bearname, actionname in invalid_actions.items():
117
        log_printer.warn("Selected default action {} for bear {} does "
118
                         "not exist. Ignoring action.".format(
119
                             repr(actionname),
120
                             repr(bearname)))
121
122
    if len(default_actions) == 0:
123
        # There's nothing to auto-apply.
124
        return results
125
126
    not_processed_results = []
127
    for result in results:
128
        try:
129
            action = default_actions[result.origin]
130
        except KeyError:
131
            not_processed_results.append(result)
132
            continue
133
134
        if not action.is_applicable(result, file_dict, file_diff_dict):
135
            log_printer.warn("Selected default action {} for bear {} is not "
136
                             "applicable. Action not applied.".format(
137
                                 repr(action.get_metadata().name),
138
                                 repr(result.origin)))
139
            not_processed_results.append(result)
140
            continue
141
142
        try:
143
            action().apply_from_section(result,
144
                                        file_dict,
145
                                        file_diff_dict,
146
                                        section)
147
            log_printer.info("Applied {} for {}.".format(
148
                repr(action.get_metadata().name),
149
                repr(result.origin)))
150
        except Exception as ex:
151
            log_printer.log_exception(
152
                "Failed to execute action {}.".format(
153
                    repr(action.get_metadata().name)),
154
                ex)
155
            log_printer.debug("-> for result " + repr(result) + ".")
156
157
    return not_processed_results
158
159
160
def print_result(results,
161
                 file_dict,
162
                 retval,
163
                 print_results,
164
                 section,
165
                 log_printer,
166
                 file_diff_dict,
167
                 ignore_ranges):
168
    """
169
    Takes the results produced by each bear and gives them to the print_results
170
    method to present to the user.
171
172
    :param results:        A list of results.
173
    :param file_dict:      A dictionary containing the name of files and its
174
                           contents.
175
    :param retval:         It is True if no results were yielded ever before.
176
                           If it is False this function will return False no
177
                           matter what happens. Else it depends on if this
178
                           invocation yields results.
179
    :param print_results:  A function that prints all given results appropriate
180
                           to the output medium.
181
    :param file_diff_dict: A dictionary that contains filenames as keys and
182
                           diff objects as values.
183
    :param ignore_ranges:  A list of SourceRanges. Results that affect code in
184
                           any of those ranges will be ignored.
185
    :return:               Returns False if any results were yielded. Else
186
                           True.
187
    """
188
    min_severity_str = str(section.get('min_severity', 'INFO')).upper()
189
    min_severity = RESULT_SEVERITY.str_dict.get(min_severity_str, 'INFO')
190
    results = list(filter(lambda result:
191
                              type(result) is Result and
192
                              result.severity >= min_severity and
193
                              not result.to_ignore(ignore_ranges),
194
                          results))
195
196
    results = autoapply_actions(results,
197
                                file_dict,
198
                                file_diff_dict,
199
                                section,
200
                                log_printer)
201
202
    print_results(log_printer, section, results, file_dict, file_diff_dict)
203
    return retval or len(results) > 0
204
205
206
def get_file_dict(filename_list, log_printer):
207
    """
208
    Reads all files into a dictionary.
209
210
    :param filename_list: List of names of paths to files to get contents of.
211
    :param log_printer:   The logger which logs errors.
212
    :return:              Reads the content of each file into a dictionary
213
                          with filenames as keys.
214
    """
215
    file_dict = {}
216
    for filename in filename_list:
217
        try:
218
            with open(filename, "r", encoding="utf-8") as _file:
219
                file_dict[filename] = _file.readlines()
220
        except UnicodeDecodeError:
221
            log_printer.warn("Failed to read file '{}'. It seems to contain "
222
                             "non-unicode characters. Leaving it "
223
                             "out.".format(filename))
224
        except Exception as exception:  # pragma: no cover
225
            log_printer.log_exception("Failed to read file '{}' because of "
226
                                      "an unknown error. Leaving it "
227
                                      "out.".format(filename),
228
                                      exception,
229
                                      log_level=LOG_LEVEL.WARNING)
230
231
    return file_dict
232
233
234
def filter_raising_callables(it, exception, *args, **kwargs):
235
    """
236
    Filters all callable items inside the given iterator that raise the
237
    given exceptions.
238
239
    :param it:        The iterator to filter.
240
    :param exception: The (tuple of) exception(s) to filter for.
241
    :param args:      Positional arguments to pass to the callable.
242
    :param kwargs:    Keyword arguments to pass to the callable.
243
    """
244
    for elem in it:
245
        try:
246
            yield elem(*args, **kwargs)
247
        except exception:
248
            pass
249
250
251
def instantiate_bears(section,
252
                      local_bear_list,
253
                      global_bear_list,
254
                      file_dict,
255
                      message_queue):
256
    """
257
    Instantiates each bear with the arguments it needs.
258
259
    :param section:          The section the bears belong to.
260
    :param local_bear_list:  List of local bear classes to instantiate.
261
    :param global_bear_list: List of global bear classes to instantiate.
262
    :param file_dict:        Dictionary containing filenames and their
263
                             contents.
264
    :param message_queue:    Queue responsible to maintain the messages
265
                             delivered by the bears.
266
    :return:                 The local and global bear instance lists.
267
    """
268
    local_bear_list = [bear
269
                       for bear in filter_raising_callables(
270
                           local_bear_list,
271
                           RuntimeError,
272
                           section,
273
                           message_queue,
274
                           timeout=0.1)]
275
276
    global_bear_list = [bear
277
                        for bear in filter_raising_callables(
278
                            global_bear_list,
279
                            RuntimeError,
280
                            file_dict,
281
                            section,
282
                            message_queue,
283
                            timeout=0.1)]
284
285
    return local_bear_list, global_bear_list
286
287
288
def instantiate_processes(section,
289
                          local_bear_list,
290
                          global_bear_list,
291
                          job_count,
292
                          log_printer):
293
    """
294
    Instantiate the number of processes that will run bears which will be
295
    responsible for running bears in a multiprocessing environment.
296
297
    :param section:          The section the bears belong to.
298
    :param local_bear_list:  List of local bears belonging to the section.
299
    :param global_bear_list: List of global bears belonging to the section.
300
    :param job_count:        Max number of processes to create.
301
    :param log_printer:      The log printer to warn to.
302
    :return:                 A tuple containing a list of processes,
303
                             and the arguments passed to each process which are
304
                             the same for each object.
305
    """
306
    filename_list = collect_files(path_list(section.get('files', "")),
307
                                  path_list(section.get('ignore', "")))
308
    file_dict = get_file_dict(filename_list, log_printer)
309
310
    manager = multiprocessing.Manager()
311
    global_bear_queue = multiprocessing.Queue()
312
    filename_queue = multiprocessing.Queue()
313
    local_result_dict = manager.dict()
314
    global_result_dict = manager.dict()
315
    message_queue = multiprocessing.Queue()
316
    control_queue = multiprocessing.Queue()
317
318
    bear_runner_args = {"file_name_queue": filename_queue,
319
                        "local_bear_list": local_bear_list,
320
                        "global_bear_list": global_bear_list,
321
                        "global_bear_queue": global_bear_queue,
322
                        "file_dict": file_dict,
323
                        "local_result_dict": local_result_dict,
324
                        "global_result_dict": global_result_dict,
325
                        "message_queue": message_queue,
326
                        "control_queue": control_queue,
327
                        "timeout": 0.1}
328
329
    local_bear_list[:], global_bear_list[:] = instantiate_bears(
330
        section,
331
        local_bear_list,
332
        global_bear_list,
333
        file_dict,
334
        message_queue)
335
336
    fill_queue(filename_queue, file_dict.keys())
337
    fill_queue(global_bear_queue, range(len(global_bear_list)))
338
339
    return ([multiprocessing.Process(target=run, kwargs=bear_runner_args)
340
             for i in range(job_count)],
341
            bear_runner_args)
342
343
344
def get_ignore_scope(line, keyword):
345
    """
346
    Retrieves the bears that are to be ignored defined in the given line.
347
    :param line:    The line containing the ignore declaration.
348
    :param keyword: The keyword that was found. Everything after the rightmost
349
                    occurrence of it will be considered for the scope.
350
    :return:        A list of lower cased bearnames or an empty list (-> "all")
351
    """
352
    toignore = line[line.rfind(keyword) + len(keyword):]
353
    if toignore.startswith("all"):
354
        return []
355
    else:
356
        return list(StringConverter(toignore, list_delimiters=', '))
357
358
359
def yield_ignore_ranges(file_dict):
360
    """
361
    Yields tuples of affected bears and a SourceRange that shall be ignored for
362
    those.
363
364
    :param file_dict: The file dictionary.
365
    """
366
    for filename, file in file_dict.items():
367
        start = None
368
        bears = []
369
        for line_number, line in enumerate(file, start=1):
370
            line = line.lower()
371
            if "start ignoring " in line:
372
                start = line_number
373
                bears = get_ignore_scope(line, "start ignoring ")
374
            elif "stop ignoring" in line:
375
                if start:
376
                    yield (bears,
377
                           SourceRange.from_values(filename,
378
                                                   start,
379
                                                   end_line=line_number))
380
            elif "ignore " in line:
381
                yield (get_ignore_scope(line, "ignore "),
382
                       SourceRange.from_values(filename,
383
                                               line_number,
384
                                               end_line=line_number+1))
385
386
387
def process_queues(processes,
388
                   control_queue,
389
                   local_result_dict,
390
                   global_result_dict,
391
                   file_dict,
392
                   print_results,
393
                   section,
394
                   log_printer):
395
    """
396
    Iterate the control queue and send the results recieved to the print_result
397
    method so that they can be presented to the user.
398
399
    :param processes:          List of processes which can be used to run
400
                               Bears.
401
    :param control_queue:      Containing control elements that indicate
402
                               whether there is a result available and which
403
                               bear it belongs to.
404
    :param local_result_dict:  Dictionary containing results respective to
405
                               local bears. It is modified by the processes
406
                               i.e. results are added to it by multiple
407
                               processes.
408
    :param global_result_dict: Dictionary containing results respective to
409
                               global bears. It is modified by the processes
410
                               i.e. results are added to it by multiple
411
                               processes.
412
    :param file_dict:          Dictionary containing file contents with
413
                               filename as keys.
414
    :param print_results:      Prints all given results appropriate to the
415
                               output medium.
416
    :return:                   Return True if all bears execute succesfully and
417
                               Results were delivered to the user. Else False.
418
    """
419
    file_diff_dict = {}
420
    running_processes = get_running_processes(processes)
421
    retval = False
422
    # Number of processes working on local bears
423
    local_processes = len(processes)
424
    global_result_buffer = []
425
    ignore_ranges = list(yield_ignore_ranges(file_dict))
426
427
    # One process is the logger thread
428
    while local_processes > 1 and running_processes > 1:
429
        try:
430
            control_elem, index = control_queue.get(timeout=0.1)
431
432
            if control_elem == CONTROL_ELEMENT.LOCAL_FINISHED:
433
                local_processes -= 1
434
            elif control_elem == CONTROL_ELEMENT.LOCAL:
435
                assert local_processes != 0
436
                retval = print_result(local_result_dict[index],
437
                                      file_dict,
438
                                      retval,
439
                                      print_results,
440
                                      section,
441
                                      log_printer,
442
                                      file_diff_dict,
443
                                      ignore_ranges)
444
            elif control_elem == CONTROL_ELEMENT.GLOBAL:
445
                global_result_buffer.append(index)
446
        except queue.Empty:
447
            running_processes = get_running_processes(processes)
448
449
    # Flush global result buffer
450
    for elem in global_result_buffer:
451
        retval = print_result(global_result_dict[elem],
452
                              file_dict,
453
                              retval,
454
                              print_results,
455
                              section,
456
                              log_printer,
457
                              file_diff_dict,
458
                              ignore_ranges)
459
460
    running_processes = get_running_processes(processes)
461
    # One process is the logger thread
462
    while running_processes > 1:
463
        try:
464
            control_elem, index = control_queue.get(timeout=0.1)
465
466
            if control_elem == CONTROL_ELEMENT.GLOBAL:
467
                retval = print_result(global_result_dict[index],
468
                                      file_dict,
469
                                      retval,
470
                                      print_results,
471
                                      section,
472
                                      log_printer,
473
                                      file_diff_dict,
474
                                      ignore_ranges)
475
            else:
476
                assert control_elem == CONTROL_ELEMENT.GLOBAL_FINISHED
477
                running_processes = get_running_processes(processes)
478
479
        except queue.Empty:
480
            running_processes = get_running_processes(processes)
481
482
    return retval
483
484
485
def execute_section(section,
486
                    global_bear_list,
487
                    local_bear_list,
488
                    print_results,
489
                    log_printer):
490
    """
491
    Executes the section with the given bears.
492
493
    The execute_section method does the following things:
494
    1. Prepare a Process
495
      * Load files
496
      * Create queues
497
    2. Spawn up one or more Processes
498
    3. Output results from the Processes
499
    4. Join all processes
500
501
    :param section:          The section to execute.
502
    :param global_bear_list: List of global bears belonging to the section.
503
    :param local_bear_list:  List of local bears belonging to the section.
504
    :param print_results:    Prints all given results appropriate to the
505
                             output medium.
506
    :param log_printer:      The log_printer to warn to.
507
    :return:                 Tuple containing a bool (True if results were
508
                             yielded, False otherwise), a Manager.dict
509
                             containing all local results(filenames are key)
510
                             and a Manager.dict containing all global bear
511
                             results (bear names are key) as well as the
512
                             file dictionary.
513
    """
514
    local_bear_list = Dependencies.resolve(local_bear_list)
515
    global_bear_list = Dependencies.resolve(global_bear_list)
516
517
    try:
518
        running_processes = int(section['jobs'])
519
    except ValueError:
520
        log_printer.warn("Unable to convert setting 'jobs' into a number. "
521
                         "Falling back to CPU count.")
522
        running_processes = get_cpu_count()
523
    except IndexError:
524
        running_processes = get_cpu_count()
525
526
    processes, arg_dict = instantiate_processes(section,
527
                                                local_bear_list,
528
                                                global_bear_list,
529
                                                running_processes,
530
                                                log_printer)
531
532
    logger_thread = LogPrinterThread(arg_dict["message_queue"],
533
                                     log_printer)
534
    # Start and join the logger thread along with the processes to run bears
535
    processes.append(logger_thread)
536
537
    for runner in processes:
538
        runner.start()
539
540
    try:
541
        return (process_queues(processes,
542
                               arg_dict["control_queue"],
543
                               arg_dict["local_result_dict"],
544
                               arg_dict["global_result_dict"],
545
                               arg_dict["file_dict"],
546
                               print_results,
547
                               section,
548
                               log_printer),
549
                arg_dict["local_result_dict"],
550
                arg_dict["global_result_dict"],
551
                arg_dict["file_dict"])
552
    finally:
553
        logger_thread.running = False
554
555
        for runner in processes:
556
            runner.join()
557