diff --git a/.travis.yml b/.travis.yml index 47df024..dc15ee3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,7 @@ script: - cd example && rime help - rime test && rime clean - rime htmlify_full && rime clean + - rime markdownify_full && rime clean notifications: email: on_success: never diff --git a/example/PROJECT b/example/PROJECT index c4d1cc7..b3ac618 100644 --- a/example/PROJECT +++ b/example/PROJECT @@ -3,3 +3,4 @@ ## You can load plugins here. use_plugin('wikify') use_plugin('htmlify_full') +use_plugin('markdownify_full') diff --git a/rime/plugins/htmlify_full.py b/rime/plugins/htmlify_full.py index 238d4f2..6c1588a 100644 --- a/rime/plugins/htmlify_full.py +++ b/rime/plugins/htmlify_full.py @@ -277,7 +277,7 @@ def _GenerateHtmlFullOne(self, problem, ui): cell_judge = HTMLIFY_CELL_NA # Done. - html = ('{}{}' + html = (u'{}{}' '\n'.format( title, assignees, cell_solutions, cell_input, cell_output, cell_validator, cell_judge)) diff --git a/rime/plugins/markdownify_full.py b/rime/plugins/markdownify_full.py new file mode 100644 index 0000000..a75e700 --- /dev/null +++ b/rime/plugins/markdownify_full.py @@ -0,0 +1,315 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +import codecs +import getpass +import hashlib +import os +import os.path +import socket +import sys + +if sys.version_info[0] == 2: + import commands as builtin_commands # NOQA +else: + import subprocess as builtin_commands + +from rime.basic import codes as basic_codes # NOQA +from rime.basic import consts # NOQA +import rime.basic.targets.project # NOQA +from rime.basic import test # NOQA +from rime.core import commands as rime_commands # NOQA +from rime.core import targets # NOQA +from rime.core import taskgraph # NOQA + +MARKDOWNIFY_EMOJI_GOOD = ' :white_check_mark: ' +MARKDOWNIFY_EMOJI_NOTBAD = ' :large_blue_diamond: ' +MARKDOWNIFY_EMOJI_BAD = ' :x: ' +MARKDOWNIFY_EMOJI_NA = ' :wavy_dash: ' + + +def SafeUnicode(s): + if sys.version_info.major == 2 and not isinstance(s, unicode): # NOQA + s = s.decode('utf-8') + return s + + +def GetFileSize(dir, filename): + filepath = os.path.join(dir, filename) + if os.path.exists(filepath): + return '%dB' % os.path.getsize(filepath) + else: + return '-' + + +def GetFileHash(dir, filename): + filepath = os.path.join(dir, filename) + if os.path.exists(filepath): + f = open(filepath) + r = f.read() + f.close() + return hashlib.md5(SafeUnicode(r).encode('utf-8')).hexdigest() + else: + return '' + + +def GetMarkdownifyFileComment(dir, filename): + filepath = os.path.join(dir, filename) + if os.path.exists(filepath): + f = open(filepath) + r = f.read().strip() + f.close() + return SafeUnicode(r).replace('\n', '
') + else: + return '' + + +class Project(targets.registry.Project): + @taskgraph.task_method + def MarkdownifyFull(self, ui): + markdownFull = yield self._GenerateMarkdownFull(ui) + codecs.open("summary.md", 'w', 'utf8').write(markdownFull) + yield None + + @taskgraph.task_method + def _GenerateMarkdownFull(self, ui): + if not ui.options.skip_clean: + yield self.Clean(ui) + + # Get system information. + rev = SafeUnicode(builtin_commands.getoutput( + 'git show -s --oneline').replace('\n', ' ').replace('\r', ' ')) + username = getpass.getuser() + hostname = socket.gethostname() + + header = u'# Project Status\n\n' + info = u'このファイルは markdownify_full plugin により自動生成されています ' + info += (u'(rev.%(rev)s, uploaded by %(username)s @ %(hostname)s)\n\n' + % {'rev': rev, 'username': username, 'hostname': hostname}) + footer = u'' + + # Generate content. + markdown = u'## Summary\n\n' + markdown += ( + u'問題|担当|解答|入力|出力|入検|出検\n' + ':---|:---|:---|:---|:---|:---|:---\n' + ) + + markdownFull = u'## Detail\n\n' + + results = yield taskgraph.TaskBranch([ + self._GenerateMarkdownFullOne(problem, ui) + for problem in self.problems]) + (markdownResults, markdownFullResults) = zip(*results) + markdown += ''.join(markdownResults) + '\n' + markdownFull += ''.join(markdownFullResults) + + cc = os.getenv('CC', 'gcc') + cxx = os.getenv('CXX', 'g++') + java_home = os.getenv('JAVA_HOME') + if java_home is not None: + java = os.path.join(java_home, 'bin/java') + javac = os.path.join(java_home, 'bin/javac') + else: + java = 'java' + javac = 'javac' + environments = '## Environments\n\n' + environments += ( + '- gcc\n\t- ' + + builtin_commands.getoutput('{0} --version'.format(cc)) + + '\n') + environments += ( + '- g++\n\t- ' + + builtin_commands.getoutput('{0} --version'.format(cxx)) + + '\n') + environments += ( + '- javac\n\t- ' + + builtin_commands.getoutput('{0} -version'.format(javac)) + + '\n') + environments += ( + '- java\n\t- ' + + builtin_commands.getoutput('{0} -version'.format(java)) + + '\n') + environments += '\n' + + errors = '' + if ui.errors.HasError() or ui.errors.HasWarning(): + errors = '## Error Messages\n\n' + if ui.errors.HasError(): + errors += '- ERROR:\n' + for e in ui.errors.errors: + errors += '\t- ' + e + '\n' + if ui.errors.HasWarning(): + errors += '- WARNING:\n' + for e in ui.errors.warnings: + errors += '\t- ' + e + '\n' + errors += '\n' + + yield ( + header + info + markdown + environments + errors + markdownFull + + footer) + + @taskgraph.task_method + def _GenerateMarkdownFullOne(self, problem, ui): + yield problem.Build(ui) + + # Get status. + title = SafeUnicode(problem.title) or 'No Title' + assignees = problem.assignees + if isinstance(assignees, list): + assignees = ','.join(assignees) + assignees = SafeUnicode(assignees) + + # Get various information about the problem. + markdownFull = '### ' + title + '\n\n' + solutions = sorted(problem.solutions, key=lambda x: x.name) + solutionnames = [solution.name for solution in solutions] + + captions = [ + name.replace('-', ' ').replace('_', ' ') for name in solutionnames] + markdownFull += ('|'.join( + ['testcase', 'in', 'diff', 'md5'] + captions + + ['Comments']) + '\n') + markdownFull += '|:---' * (5 + len(captions)) + '\n' + + dics = {} + for testcase in problem.testset.ListTestCases(): + testname = os.path.splitext(os.path.basename(testcase.infile))[0] + dics[testname] = {} + results = [] + for solution in solutions: + name = solution.name + test_result = (yield problem.testset.TestSolution(solution, ui))[0] + results.append(test_result) + for (testcase, result) in test_result.results.items(): + testname = os.path.splitext( + os.path.basename(testcase.infile))[0] + dics.setdefault(testname, {})[name] = ( + result.verdict, result.time) + testnames = sorted(dics.keys()) + lists = [] + for testname in testnames: + cols = [] + for name in solutionnames: + cols.append(dics[testname].get( + name, (test.TestCaseResult.NA, None))) + lists.append((testname, cols)) + rows = [] + dir = problem.testset.out_dir + for casename, cols in lists: + rows.append( + '|'.join( + [ + casename.replace('_', ' ').replace('-', ' '), + GetFileSize(dir, casename + consts.IN_EXT), + GetFileSize(dir, casename + consts.DIFF_EXT), + '`%s`' % GetFileHash(dir, casename + consts.IN_EXT)[:7] + ] + + [self._GetMarkdownifyMessage(*t) for t in cols] + + [GetMarkdownifyFileComment(dir, casename + '.comment')] + ) + + '\n') + markdownFull += ''.join(rows) + '\n' + + # Fetch test results. + # results = yield problem.Test(ui) + + # Get various information about the problem. + num_solutions = len(results) + num_tests = len(problem.testset.ListTestCases()) + correct_solution_results = [result for result in results + if result.solution.IsCorrect()] + num_corrects = len(correct_solution_results) + num_incorrects = num_solutions - num_corrects + num_agreed = len([result for result in correct_solution_results + if result.expected]) + need_custom_judge = problem.need_custom_judge + + # Solutions: + if num_corrects >= 2: + cell_solutions = MARKDOWNIFY_EMOJI_GOOD + elif num_corrects >= 1: + cell_solutions = MARKDOWNIFY_EMOJI_NOTBAD + else: + cell_solutions = MARKDOWNIFY_EMOJI_BAD + cell_solutions += '%d+%d' % (num_corrects, num_incorrects) + + # Input: + if num_tests >= 20: + cell_input = MARKDOWNIFY_EMOJI_GOOD + str(num_tests) + else: + cell_input = MARKDOWNIFY_EMOJI_BAD + str(num_tests) + + # Output: + if num_corrects >= 2 and num_agreed == num_corrects: + cell_output = MARKDOWNIFY_EMOJI_GOOD + elif num_agreed >= 2: + cell_output = MARKDOWNIFY_EMOJI_NOTBAD + else: + cell_output = MARKDOWNIFY_EMOJI_BAD + cell_output += '%d/%d' % (num_agreed, num_corrects) + + # Validator: + if problem.testset.validators: + cell_validator = MARKDOWNIFY_EMOJI_GOOD + else: + cell_validator = MARKDOWNIFY_EMOJI_BAD + + # Judge: + if need_custom_judge: + custom_judges = [ + judge for judge in problem.testset.judges + if judge.__class__ != basic_codes.InternalDiffCode] + if custom_judges: + cell_judge = MARKDOWNIFY_EMOJI_GOOD + else: + cell_judge = MARKDOWNIFY_EMOJI_BAD + else: + cell_judge = MARKDOWNIFY_EMOJI_NA + + # Done. + markdown = (u'{}|{}|{}|{}|{}|{}|{}\n'.format( + title, assignees, cell_solutions, cell_input, + cell_output, cell_validator, cell_judge)) + + yield (markdown, markdownFull) + + def _GetMarkdownifyMessage(self, verdict, time): + if verdict is test.TestCaseResult.NA: + return MARKDOWNIFY_EMOJI_NA + str(verdict) + elif time is None: + return MARKDOWNIFY_EMOJI_BAD + str(verdict) + else: + return MARKDOWNIFY_EMOJI_GOOD + '%.2fs' % (time) + + +class MarkdownifyFull(rime_commands.CommandBase): + def __init__(self, parent): + super(MarkdownifyFull, self).__init__( + 'markdownify_full', + '', + 'Markdownify full plugin', + '', + parent) + self.AddOptionEntry(rime_commands.OptionEntry( + 's', 'skip_clean', 'skip_clean', bool, False, None, + 'Skip cleaning generated files up.' + )) + + def Run(self, obj, args, ui): + if args: + ui.console.PrintError( + 'Extra argument passed to markdownify_full command!') + return None + + if isinstance(obj, Project): + return obj.MarkdownifyFull(ui) + + ui.console.PrintError( + 'Markdownify_full is not supported for the specified target.') + return None + + +targets.registry.Override('Project', Project) + +rime_commands.registry.Add(MarkdownifyFull) diff --git a/tests/plugins_test/test_markdownify_full.py b/tests/plugins_test/test_markdownify_full.py new file mode 100644 index 0000000..bbaa178 --- /dev/null +++ b/tests/plugins_test/test_markdownify_full.py @@ -0,0 +1,36 @@ +import unittest + +import mock + +from rime.plugins import markdownify_full +from rime.util import struct + + +class TestMarkdownifyProject(unittest.TestCase): + def test_do_clean(self): + ui = mock.MagicMock() + ui.options = struct.Struct({'skip_clean': False}) + project = markdownify_full.Project('project', 'base_dir', None) + project.problems = [] + project.Clean = mock.MagicMock() + + task_graph = project._GenerateMarkdownFull(ui) + task_graph.Continue() + + project.Clean.called_once_with(ui) + + def test_skip_clean(self): + ui = mock.MagicMock() + ui.options = struct.Struct({'skip_clean': True}) + project = markdownify_full.Project('project', 'base_dir', None) + project.problems = [] + project.Clean = mock.MagicMock() + + task_graph = project._GenerateMarkdownFull(ui) + task_graph.Continue() + + project.Clean.assert_not_called() + + +if __name__ == '__main__': + unittest.main()