Repository: kmyk/online-judge-tools
Pull request #718 (2020-04-05 13:36:32)

Fix the bug of submit subcommand about Python and AtCoder's language update
AtCoder の言語更新で Python コードの提出言語判定が壊れたので修正します。
https://atcoder.jp/contests/judge-update-202004

もともと微妙にバグってたのが、`Python (3.8.2)` が部分文字列として `3` と `2` を含むので Python 3 かつ Python 2 の判定になって顕在化しました。


Patch:
diff --git a/onlinejudge/_implementation/command/submit.py b/onlinejudge/_implementation/command/submit.py
index 5950f96..97dc9f5 100644
--- a/onlinejudge/_implementation/command/submit.py
+++ b/onlinejudge/_implementation/command/submit.py
@@ -53,14 +53,14 @@ def submit(args: 'argparse.Namespace') -> None:
 
     with utils.with_cookiejar(utils.new_session_with_our_user_agent(), path=args.cookie) as sess:
         # guess or select language ids
-        langs = {language.id: {'description': language.name} for language in problem.get_available_languages(session=sess)}  # type: Dict[LanguageId, Dict[str, str]]
+        language_dict = {language.id: language.name for language in problem.get_available_languages(session=sess)}  # type: Dict[LanguageId, str]
         matched_lang_ids = None  # type: Optional[List[str]]
-        if args.language in langs:
+        if args.language in language_dict:
             matched_lang_ids = [args.language]
         else:
             if args.guess:
                 kwargs = {
-                    'language_dict': langs,
+                    'language_dict': language_dict,
                     'cxx_latest': args.guess_cxx_latest,
                     'cxx_compiler': args.guess_cxx_compiler,
                     'python_version': args.guess_python_version,
@@ -69,20 +69,20 @@ def submit(args: 'argparse.Namespace') -> None:
                 matched_lang_ids = guess_lang_ids_of_file(args.file, code, **kwargs)
                 if not matched_lang_ids:
                     log.info('failed to guess languages from the file name')
-                    matched_lang_ids = list(langs.keys())
+                    matched_lang_ids = list(language_dict.keys())
                 if args.language is not None:
                     log.info('you can use `--no-guess` option if you want to do an unusual submission')
-                    matched_lang_ids = select_ids_of_matched_languages(args.language.split(), matched_lang_ids, language_dict=langs)
+                    matched_lang_ids = select_ids_of_matched_languages(args.language.split(), matched_lang_ids, language_dict=language_dict)
             else:
                 if args.language is None:
                     matched_lang_ids = None
                 else:
-                    matched_lang_ids = select_ids_of_matched_languages(args.language.split(), list(langs.keys()), language_dict=langs)
+                    matched_lang_ids = select_ids_of_matched_languages(args.language.split(), list(language_dict.keys()), language_dict=language_dict)
 
         # report selected language ids
         if matched_lang_ids is not None and len(matched_lang_ids) == 1:
             args.language = matched_lang_ids[0]
-            log.info('chosen language: %s (%s)', args.language, langs[LanguageId(args.language)]['description'])
+            log.info('chosen language: %s (%s)', args.language, language_dict[LanguageId(args.language)])
         else:
             if matched_lang_ids is None:
                 log.error('language is unknown')
@@ -93,8 +93,8 @@ def submit(args: 'argparse.Namespace') -> None:
             else:
                 log.error('Matched languages were not narrowed down to one.')
                 log.info('You have to choose:')
-            for lang_id in sorted(matched_lang_ids or langs.keys()):
-                log.emit('%s (%s)', lang_id, langs[LanguageId(lang_id)]['description'])
+            for lang_id in sorted(matched_lang_ids or language_dict.keys()):
+                log.emit('%s (%s)', lang_id, language_dict[LanguageId(lang_id)])
             sys.exit(1)
 
         # confirm
@@ -145,7 +145,7 @@ def submit(args: 'argparse.Namespace') -> None:
 def select_ids_of_matched_languages(words: List[str], lang_ids: List[str], language_dict, split: bool = False, remove: bool = False) -> List[str]:
     result = []
     for lang_id in lang_ids:
-        desc = language_dict[lang_id]['description'].lower()
+        desc = language_dict[lang_id].lower()
         if split:
             desc = desc.split()
         pred = all([word.lower() in desc for word in words])
@@ -156,13 +156,72 @@ def select_ids_of_matched_languages(words: List[str], lang_ids: List[str], langu
     return result
 
 
+def is_cplusplus_description(description: str) -> bool:
+    # Here, 'clang' is not used as intended. Think about strings like "C++ (Clang)", "Clang++" (this includes "g++" as a substring), or "C (Clang)".
+    return 'c++' in description.lower() or 'g++' in description.lower()
+
+
+def parse_cplusplus_compiler(description: str) -> str:
+    """
+    :param description: must be for C++
+    """
+
+    assert is_cplusplus_description(description)
+    if 'clang' in description.lower():
+        return 'clang'
+    if 'gcc' in description.lower() or 'g++' in description.lower():
+        return 'gcc'
+    return 'gcc'  # by default
+
+
+def parse_cplusplus_version(description: str) -> Optional[str]:
+    """
+    :param description: must be for C++
+    """
+
+    assert is_cplusplus_description(description)
+    match = re.search(r'[CG]\+\+\s?(\d\w)\b', description)
+    if match:
+        return match.group(1)
+    return None
+
+
+def is_python_description(description: str) -> bool:
+    return 'python' in description.lower() or 'pypy' in description.lower()
+
+
+def parse_python_version(description: str) -> Optional[int]:
+    """
+    :param description: must be for Python
+    """
+
+    assert is_python_description(description)
+    match = re.match(r'([23])\.(?:\d+(?:\.\d+)?|x)', description)
+    if match:
+        return int(match.group(1))
+    match = re.match(r'(?:Python|PyPy) *\(?([23])', description, re.IGNORECASE)
+    if match:
+        return int(match.group(1))
+    return None
+
+
+def parse_python_interpreter(description: str) -> str:
+    """
+    :param description: must be for Python
+    """
+
+    assert is_python_description(description)
+    if 'pypy' in description.lower():
+        return 'pypy'
+    else:
+        return 'cpython'
+
+
 def guess_lang_ids_of_file(filename: pathlib.Path, code: bytes, language_dict, cxx_latest: bool = False, cxx_compiler: str = 'all', python_version: str = 'all', python_interpreter: str = 'all') -> List[str]:
-    assert cxx_compiler.lower() in ('gcc', 'clang', 'all')
-    assert python_version.lower() in ('2', '3', 'auto', 'all')
-    assert python_interpreter.lower() in ('cpython', 'pypy', 'all')
+    assert cxx_compiler in ('gcc', 'clang', 'all')
+    assert python_version in ('2', '3', 'auto', 'all')
+    assert python_interpreter in ('cpython', 'pypy', 'all')
 
-    select_words = (lambda words, lang_ids, **kwargs: select_ids_of_matched_languages(words, lang_ids, language_dict=language_dict, **kwargs))
-    select = (lambda word, lang_ids, **kwargs: select_words([word], lang_ids, **kwargs))
     ext = filename.suffix
     lang_ids = language_dict.keys()
 
@@ -172,70 +231,72 @@ def guess_lang_ids_of_file(filename: pathlib.Path, code: bytes, language_dict, c
     if ext in ('cpp', 'cxx', 'cc', 'C'):
         log.debug('language guessing: C++')
         # memo: https://stackoverflow.com/questions/1545080/c-code-file-extension-cc-vs-cpp
-        lang_ids = list(set(select('c++', lang_ids) + select('g++', lang_ids)))
+        lang_ids = list(filter(lambda lang_id: is_cplusplus_description(language_dict[lang_id]), lang_ids))
         if not lang_ids:
             return []
         log.debug('all lang ids for C++: %s', lang_ids)
 
         # compiler
-        select_gcc = lambda ids: list(set(select('gcc', ids) + select('clang', select('g++', ids), remove=True)))
-        if select_gcc(lang_ids) and select('clang', lang_ids):
+        found_gcc = False
+        found_clang = False
+        for lang_id in lang_ids:
+            compiler = parse_cplusplus_compiler(language_dict[lang_id])
+            if compiler == 'gcc':
+                found_gcc = True
+            elif compiler == 'clang':
+                found_clang = True
+        if found_gcc and found_clang:
             log.status('both GCC and Clang are available for C++ compiler')
-            if cxx_compiler.lower() == 'gcc':
+            if cxx_compiler == 'gcc':
                 log.status('use: GCC')
-                lang_ids = select_gcc(lang_ids)
-            elif cxx_compiler.lower() == 'clang':
+                lang_ids = list(filter(lambda lang_id: parse_cplusplus_compiler(language_dict[lang_id]) in ('gcc', None), lang_ids))
+            elif cxx_compiler == 'clang':
                 log.status('use: Clang')
-                lang_ids = select('clang', lang_ids)
+                lang_ids = list(filter(lambda lang_id: parse_cplusplus_compiler(language_dict[lang_id]) in ('clang', None), lang_ids))
             else:
-                assert cxx_compiler.lower() == 'all'
+                assert cxx_compiler == 'all'
         log.debug('lang ids after compiler filter: %s', lang_ids)
 
         # version
         if cxx_latest:
-            saved_ids = lang_ids
+            saved_lang_ids = lang_ids
             lang_ids = []
-            for compiler in (None, 'gcc', 'clang'):  # use the latest for each compiler
-                version_of = {}
-                if compiler == 'gcc':
-                    ids = select_gcc(saved_ids)
-                elif compiler == 'clang':
-                    ids = select('clang', saved_ids)
-                else:
-                    ids = saved_ids
+            for compiler in ('gcc', 'clang'):  # use the latest for each compiler
+                ids = list(filter(lambda lang_id: parse_cplusplus_compiler(language_dict[lang_id]) in (compiler, None), saved_lang_ids))
                 if not ids:
                     continue
-                for lang_id in ids:
-                    m = re.search(r'[cg]\+\+\w\w', language_dict[lang_id]['description'].lower())
-                    if m:
-                        version_of[lang_id] = m.group(0)
-                ids.sort(key=lambda lang_id: version_of.get(lang_id, ''))
+                ids.sort(key=lambda lang_id: (parse_cplusplus_version(language_dict[lang_id]) or '', language_dict[lang_id]))
                 lang_ids += [ids[-1]]  # since C++11 < C++1y < ... as strings
-            lang_ids = list(set(lang_ids))
         log.debug('lang ids after version filter: %s', lang_ids)
 
         assert lang_ids
+        lang_ids = sorted(set(lang_ids))
         return lang_ids
 
     elif ext == 'py':
         log.debug('language guessing: Python')
-        if select('pypy', language_dict.keys()):
-            log.status('PyPy is available for Python interpreter')
 
         # interpreter
-        lang_ids = []
-        if python_interpreter.lower() in ('cpython', 'all'):
-            lang_ids += select('python', language_dict.keys())
-        elif python_interpreter.lower() in ('pypy', 'all') or not lang_ids:
-            lang_ids += select('pypy', language_dict.keys())
+        lang_ids = list(filter(lambda lang_id: is_python_description(language_dict[lang_id]), lang_ids))
+        if any([parse_python_interpreter(language_dict[lang_id]) == 'pypy' for lang_id in lang_ids]):
+            log.status('PyPy is available for Python interpreter')
+        if python_interpreter != 'all':
+            lang_ids = list(filter(lambda lang_id: parse_python_interpreter(language_dict[lang_id]) == python_interpreter, lang_ids))
 
         # version
-        two_found = select_words(['python', '2'], lang_ids) or select_words(['pypy', '2'], lang_ids)
-        three_found = select_words(['python', '3'], lang_ids) or select_words(['pypy', '3'], lang_ids)
+        three_found = False
+        two_found = False
+        for lang_id in lang_ids:
+            version = parse_python_version(language_dict[lang_id])
+            log.debug('%s (%s) is recognized as Python %s', lang_id, language_dict[lang_id], str(version or 'unknown'))
+            if version == 3:
+                three_found = True
+            if version == 2:
+                two_found = True
         if two_found and three_found:
             log.status('both Python2 and Python3 are available for version of Python')
             if python_version in ('2', '3'):
-                versions = [int(python_version)]
+                versions = [int(python_version)]  # type: List[Optional[int]]
             elif python_version == 'all':
                 versions = [2, 3]
             else:
@@ -253,16 +314,9 @@ def guess_lang_ids_of_file(filename: pathlib.Path, code: bytes, language_dict, c
                     log.status('no version info in code')
                     versions = [3]
             log.status('use: %s', ', '.join(map(str, versions)))
+            lang_ids = list(filter(lambda lang_id: parse_python_version(language_dict[lang_id]) in versions + [None], lang_ids))
 
-            saved_ids = lang_ids
-            lang_ids = []
-            for version in versions:
-                lang_ids += select('python%d' % version, saved_ids)
-                lang_ids += select('python %d' % version, saved_ids)
-                lang_ids += select('pypy%d' % version, saved_ids)
-                lang_ids += select('pypy %d' % version, saved_ids)
-
-        lang_ids = list(set(lang_ids))
+        lang_ids = sorted(set(lang_ids))
         return lang_ids
 
     else:
@@ -314,8 +368,8 @@ def guess_lang_ids_of_file(filename: pathlib.Path, code: bytes, language_dict, c
         for data in table:
             if ext in data['exts']:
                 for name in data['names']:
-                    lang_ids += select(name, language_dict.keys(), split=data.get('split', False))
-        return list(set(lang_ids))
+                    lang_ids += select_ids_of_matched_languages([name], language_dict.keys(), language_dict=language_dict, split=data.get('split', False))
+        return sorted(set(lang_ids))
 
 
 def format_code(code: bytes, dos2unix: bool = False, rstrip: bool = False) -> bytes:


Test patch:
diff --git a/tests/implementation_language_guessing.py b/tests/implementation_language_guessing.py
new file mode 100644
index 0000000..c2306f9
--- /dev/null
+++ b/tests/implementation_language_guessing.py
@@ -0,0 +1,404 @@
+import pathlib
+import textwrap
+import unittest
+
+import onlinejudge._implementation.command.submit as submit
+from onlinejudge.type import Language, LanguageId
+
+# https://atcoder.jp/contests/language-test-ver1
+languages_atcoder_3 = [
+    Language(id=LanguageId('3001'), name='Bash (GNU bash v4.3.11)'),
+    Language(id=LanguageId('3002'), name='C (GCC 5.4.1)'),
+    Language(id=LanguageId('3003'), name='C++14 (GCC 5.4.1)'),
+    Language(id=LanguageId('3004'), name='C (Clang 3.8.0)'),
+    Language(id=LanguageId('3005'), name='C++14 (Clang 3.8.0)'),
+    Language(id=LanguageId('3006'), name='C# (Mono 4.6.2.0)'),
+    Language(id=LanguageId('3007'), name='Clojure (1.8.0)'),
+    Language(id=LanguageId('3008'), name='Common Lisp (SBCL 1.1.14)'),
+    Language(id=LanguageId('3009'), name='D (DMD64 v2.070.1)'),
+    Language(id=LanguageId('3010'), name='D (LDC 0.17.0)'),
+    Language(id=LanguageId('3011'), name='D (GDC 4.9.4)'),
+    Language(id=LanguageId('3012'), name='Fortran (gfortran v4.8.4)'),
+    Language(id=LanguageId('3013'), name='Go (1.6)'),
+    Language(id=LanguageId('3014'), name='Haskell (GHC 7.10.3)'),
+    Language(id=LanguageId('3015'), name='Java7 (OpenJDK 1.7.0)'),
+    Language(id=LanguageId('3016'), name='Java8 (OpenJDK 1.8.0)'),
+    Language(id=LanguageId('3017'), name='JavaScript (node.js v5.12)'),
+    Language(id=LanguageId('3018'), name='OCaml (4.02.3)'),
+    Language(id=LanguageId('3019'), name='Pascal (FPC 2.6.2)'),
+    Language(id=LanguageId('3020'), name='Perl (v5.18.2)'),
+    Language(id=LanguageId('3021'), name='PHP (5.6.30)'),
+    Language(id=LanguageId('3022'), name='Python2 (2.7.6)'),
+    Language(id=LanguageId('3023'), name='Python3 (3.4.3)'),
+    Language(id=LanguageId('3024'), name='Ruby (2.3.3)'),
+    Language(id=LanguageId('3025'), name='Scala (2.11.7)'),
+    Language(id=LanguageId('3026'), name='Scheme (Gauche 0.9.3.3)'),
+    Language(id=LanguageId('3027'), name='Text (cat)'),
+    Language(id=LanguageId('3028'), name='Visual Basic (Mono 4.0.1)'),
+    Language(id=LanguageId('3029'), name='C++ (GCC 5.4.1)'),
+    Language(id=LanguageId('3030'), name='C++ (Clang 3.8.0)'),
+    Language(id=LanguageId('3501'), name='Objective-C (GCC 5.3.0)'),
+    Language(id=LanguageId('3502'), name='Objective-C (Clang3.8.0)'),
+    Language(id=LanguageId('3503'), name='Swift (swift-2.2-RELEASE)'),
+    Language(id=LanguageId('3504'), name='Rust (1.15.1)'),
+    Language(id=LanguageId('3505'), name='Sed (GNU sed 4.2.2)'),
+    Language(id=LanguageId('3506'), name='Awk (mawk 1.3.3)'),
+    Language(id=LanguageId('3507'), name='Brainfuck (bf 20041219)'),
+    Language(id=LanguageId('3508'), name='Standard ML (MLton 20100608)'),
+    Language(id=LanguageId('3509'), name='PyPy2 (5.6.0)'),
+    Language(id=LanguageId('3510'), name='PyPy3 (2.4.0)'),
+    Language(id=LanguageId('3511'), name='Crystal (0.20.5)'),
+    Language(id=LanguageId('3512'), name='F# (Mono 4.0)'),
+    Language(id=LanguageId('3513'), name='Unlambda (0.1.3)'),
+    Language(id=LanguageId('3514'), name='Lua (5.3.2)'),
+    Language(id=LanguageId('3515'), name='LuaJIT (2.0.4)'),
+    Language(id=LanguageId('3516'), name='MoonScript (0.5.0)'),
+    Language(id=LanguageId('3517'), name='Ceylon (1.2.1)'),
+    Language(id=LanguageId('3518'), name='Julia (0.5.0)'),
+    Language(id=LanguageId('3519'), name='Octave (4.0.2)'),
+    Language(id=LanguageId('3520'), name='Nim (0.13.0)'),
+    Language(id=LanguageId('3521'), name='TypeScript (2.1.6)'),
+    Language(id=LanguageId('3522'), name='Perl6 (rakudo-star 2016.01)'),
+    Language(id=LanguageId('3523'), name='Kotlin (1.0.0)'),
+    Language(id=LanguageId('3524'), name='PHP7 (7.0.15)'),
+]
+
+# https://atcoder.jp/contests/judge-update-202004
+languages_atcoder_4 = [
+    Language(id=LanguageId('4001'), name='C (GCC 9.2.1)'),
+    Language(id=LanguageId('4002'), name='C (Clang 10.0.0)'),
+    Language(id=LanguageId('4003'), name='C++ (GCC 9.2.1)'),
+    Language(id=LanguageId('4004'), name='C++ (Clang 10.0.0)'),
+    Language(id=LanguageId('4005'), name='Java (OpenJDK 11.0.6)'),
+    Language(id=LanguageId('4006'), name='Python (3.8.2)'),
+    Language(id=LanguageId('4007'), name='Bash (5.0.11)'),
+    Language(id=LanguageId('4008'), name='bc (1.07.1)'),
+    Language(id=LanguageId('4009'), name='Awk (GNU Awk 4.1.4)'),
+    Language(id=LanguageId('4010'), name='C# (.NET Core 3.1.201)'),
+    Language(id=LanguageId('4011'), name='C# (Mono-mcs 6.8.0.105)'),
+    Language(id=LanguageId('4012'), name='C# (Mono-csc 3.5.0)'),
+    Language(id=LanguageId('4013'), name='Clojure (1.10.1.536)'),
+    Language(id=LanguageId('4014'), name='Crystal (0.33.0)'),
+    Language(id=LanguageId('4015'), name='D (DMD 2.091.0)'),
+    Language(id=LanguageId('4016'), name='D (GDC 9.2.1)'),
+    Language(id=LanguageId('4017'), name='D (LDC 1.20.1)'),
+    Language(id=LanguageId('4018'), name='Dart (2.7.2)'),
+    Language(id=LanguageId('4019'), name='dc (1.4.1)'),
+    Language(id=LanguageId('4020'), name='Erlang (22.3)'),
+    Language(id=LanguageId('4021'), name='Elixir (1.10.2)'),
+    Language(id=LanguageId('4022'), name='F# (.NET Core 3.1.201)'),
+    Language(id=LanguageId('4023'), name='F# (Mono 10.2.3)'),
+    Language(id=LanguageId('4024'), name='Forth (gforth 0.7.3)'),
+    Language(id=LanguageId('4025'), name='Fortran(GNU Fortran 9.2.1)'),
+    Language(id=LanguageId('4026'), name='Go (1.14.1)'),
+    Language(id=LanguageId('4027'), name='Haskell (GHC 8.8.3)'),
+    Language(id=LanguageId('4028'), name='Haxe (4.0.3); js'),
+    Language(id=LanguageId('4029'), name='Haxe (4.0.3); Java'),
+    Language(id=LanguageId('4030'), name='JavaScript (Node.js 12.16.1)'),
+    Language(id=LanguageId('4031'), name='Julia (1.4.0)'),
+    Language(id=LanguageId('4032'), name='Kotlin (1.3.71)'),
+    Language(id=LanguageId('4033'), name='Lua (Lua 5.3.5)'),
+    Language(id=LanguageId('4034'), name='Lua (LuaJIT 2.1.0)'),
+    Language(id=LanguageId('4035'), name='Dash (0.5.8)'),
+    Language(id=LanguageId('4036'), name='Nim (1.0.6)'),
+    Language(id=LanguageId('4037'), name='Objective-C (Clang 10.0.0)'),
+    Language(id=LanguageId('4038'), name='Common Lisp (SBCL 2.0.3)'),
+    Language(id=LanguageId('4039'), name='OCaml (4.10.0)'),
+    Language(id=LanguageId('4040'), name='Octave (5.2.0)'),
+    Language(id=LanguageId('4041'), name='Pascal (FPC 3.0.4)'),
+    Language(id=LanguageId('4042'), name='Perl (5.26.1)'),
+    Language(id=LanguageId('4043'), name='Raku (Rakudo 2020.02.1)'),
+    Language(id=LanguageId('4044'), name='PHP (7.4.4)'),
+    Language(id=LanguageId('4045'), name='Prolog (SWI-Prolog 8.0.3)'),
+    Language(id=LanguageId('4046'), name='PyPy2 (7.3.0)'),
+    Language(id=LanguageId('4047'), name='PyPy3 (7.3.0)'),
+    Language(id=LanguageId('4048'), name='Racket (7.6)'),
+    Language(id=LanguageId('4049'), name='Ruby (2.7.1)'),
+    Language(id=LanguageId('4050'), name='Rust (1.42.0)'),
+    Language(id=LanguageId('4051'), name='Scala (2.13.1)'),
+    Language(id=LanguageId('4052'), name='Java (OpenJDK 1.8.0)'),
+    Language(id=LanguageId('4053'), name='Scheme (Gauche 0.9.9)'),
+    Language(id=LanguageId('4054'), name='Standard ML (MLton 20130715)'),
+    Language(id=LanguageId('4055'), name='Swift (5.2.1)'),
+    Language(id=LanguageId('4056'), name='Text (cat 8.28)'),
+    Language(id=LanguageId('4057'), name='TypeScript (3.8)'),
+    Language(id=LanguageId('4058'), name='Visual Basic (.NET Core 3.1.101)'),
+    Language(id=LanguageId('4059'), name='Zsh (5.4.2)'),
+    Language(id=LanguageId('4060'), name='COBOL - Fixed (OpenCOBOL 1.1.0)'),
+    Language(id=LanguageId('4061'), name='COBOL - Free (OpenCOBOL 1.1.0)'),
+    Language(id=LanguageId('4062'), name='Brainfuck (bf 20041219)'),
+    Language(id=LanguageId('4063'), name='Ada2012 (GNAT 9.2.1)'),
+    Language(id=LanguageId('4064'), name='Unlambda (2.0.0)'),
+    Language(id=LanguageId('4065'), name='Cython (0.29.16)'),
+    Language(id=LanguageId('4066'), name='Sed (4.4)'),
+    Language(id=LanguageId('4067'), name='Vim (8.2.0460)'),
+]
+
+# 2020-04-05
+languages_codeforces = [
+    Language(id=LanguageId('43'), name='GNU GCC C11 5.1.0'),
+    Language(id=LanguageId('52'), name='Clang++17 Diagnostics'),
+    Language(id=LanguageId('42'), name='GNU G++11 5.1.0'),
+    Language(id=LanguageId('50'), name='GNU G++14 6.4.0'),
+    Language(id=LanguageId('54'), name='GNU G++17 7.3.0'),
+    Language(id=LanguageId('2'), name='Microsoft Visual C++ 2010'),
+    Language(id=LanguageId('59'), name='Microsoft Visual C++ 2017'),
+    Language(id=LanguageId('61'), name='GNU G++17 9.2.0 (64 bit, msys 2)'),
+    Language(id=LanguageId('9'), name='C# Mono 5.18'),
+    Language(id=LanguageId('28'), name='D DMD32 v2.091.0'),
+    Language(id=LanguageId('32'), name='Go 1.14'),
+    Language(id=LanguageId('12'), name='Haskell GHC 8.6.3'),
+    Language(id=LanguageId('60'), name='Java 11.0.5'),
+    Language(id=LanguageId('36'), name='Java 1.8.0_162'),
+    Language(id=LanguageId('48'), name='Kotlin 1.3.70'),
+    Language(id=LanguageId('19'), name='OCaml 4.02.1'),
+    Language(id=LanguageId('3'), name='Delphi 7'),
+    Language(id=LanguageId('4'), name='Free Pascal 3.0.2'),
+    Language(id=LanguageId('51'), name='PascalABC.NET 3.4.2'),
+    Language(id=LanguageId('13'), name='Perl 5.20.1'),
+    Language(id=LanguageId('6'), name='PHP 7.2.13'),
+    Language(id=LanguageId('7'), name='Python 2.7.15'),
+    Language(id=LanguageId('31'), name='Python 3.7.2'),
+    Language(id=LanguageId('40'), name='PyPy 2.7 (7.2.0)'),
+    Language(id=LanguageId('41'), name='PyPy 3.6 (7.2.0)'),
+    Language(id=LanguageId('8'), name='Ruby 2.0.0p645'),
+    Language(id=LanguageId('49'), name='Rust 1.42.0'),
+    Language(id=LanguageId('20'), name='Scala 2.12.8'),
+    Language(id=LanguageId('34'), name='JavaScript V8 4.8.0'),
+    Language(id=LanguageId('55'), name='Node.js 9.4.0'),
+]
+
+
+class LanguageGuessingCPlusPlusTest(unittest.TestCase):
+    def test_atcoder_3_gcc(self):
+        languages = languages_atcoder_3
+        filename = 'main.cpp'
+        code = 'int main() {}\n'
+        cxx_latest = True
+        cxx_compiler = 'gcc'
+        expected = ['3003']
+
+        language_dict = {language.id: language.name for language in languages}  # type: Dict[LanguageId, str]
+        self.assertEqual(submit.guess_lang_ids_of_file(filename=pathlib.Path(filename), code=code.encode(), language_dict=language_dict, cxx_latest=cxx_latest, cxx_compiler=cxx_compiler), expected)
+
+    def test_atcoder_3_clang(self):
+        languages = languages_atcoder_3
+        filename = 'main.cpp'
+        code = 'int main() {}\n'
+        cxx_latest = True
+        cxx_compiler = 'clang'
+        expected = ['3005']
+
+        language_dict = {language.id: language.name for language in languages}  # type: Dict[LanguageId, str]
+        self.assertEqual(submit.guess_lang_ids_of_file(filename=pathlib.Path(filename), code=code.encode(), language_dict=language_dict, cxx_latest=cxx_latest, cxx_compiler=cxx_compiler), expected)
+
+    def test_atcoder_4_gcc(self):
+        languages = languages_atcoder_4
+        filename = 'main.cpp'
+        code = 'int main() {}\n'
+        cxx_latest = True
+        cxx_compiler = 'gcc'
+        expected = ['4003']
+
+        language_dict = {language.id: language.name for language in languages}  # type: Dict[LanguageId, str]
+        self.assertEqual(submit.guess_lang_ids_of_file(filename=pathlib.Path(filename), code=code.encode(), language_dict=language_dict, cxx_latest=cxx_latest, cxx_compiler=cxx_compiler), expected)
+
+    def test_atcoder_4_clang(self):
+        languages = languages_atcoder_4
+        filename = 'main.cpp'
+        code = 'int main() {}\n'
+        cxx_latest = True
+        cxx_compiler = 'clang'
+        expected = ['4004']
+
+        language_dict = {language.id: language.name for language in languages}  # type: Dict[LanguageId, str]
+        self.assertEqual(submit.guess_lang_ids_of_file(filename=pathlib.Path(filename), code=code.encode(), language_dict=language_dict, cxx_latest=cxx_latest, cxx_compiler=cxx_compiler), expected)
+
+    def test_codeforces_gcc(self):
+        languages = languages_codeforces
+        filename = 'main.cpp'
+        code = 'int main() {}\n'
+        cxx_latest = True
+        cxx_compiler = 'gcc'
+        expected = ['61']
+
+        language_dict = {language.id: language.name for language in languages}  # type: Dict[LanguageId, str]
+        self.assertEqual(submit.guess_lang_ids_of_file(filename=pathlib.Path(filename), code=code.encode(), language_dict=language_dict, cxx_latest=cxx_latest, cxx_compiler=cxx_compiler), expected)
+
+    def test_codeforces_clang(self):
+        languages = languages_codeforces
+        filename = 'main.cpp'
+        code = 'int main() {}\n'
+        cxx_latest = True
+        cxx_compiler = 'clang'
+        expected = ['52']
+
+        language_dict = {language.id: language.name for language in languages}  # type: Dict[LanguageId, str]
+        self.assertEqual(submit.guess_lang_ids_of_file(filename=pathlib.Path(filename), code=code.encode(), language_dict=language_dict, cxx_latest=cxx_latest, cxx_compiler=cxx_compiler), expected)
+
+
+class LanguageGuessingPythonTest(unittest.TestCase):
+    def test_atcoder_3_python_2(self):
+        languages = languages_atcoder_3
+        filename = 'main.py'
+        code = textwrap.dedent("""\
+            #!/usr/bin/env python2
+            print("hello")
+            """)
+        python_version = 'auto'
+        python_interpreter = 'cpython'
+        expected = ['3022']
+
+        language_dict = {language.id: language.name for language in languages}  # type: Dict[LanguageId, str]
+        self.assertEqual(submit.guess_lang_ids_of_file(filename=pathlib.Path(filename), code=code.encode(), language_dict=language_dict, python_version=python_version, python_interpreter=python_interpreter), expected)
+
+    def test_atcoder_3_python_3(self):
+        languages = languages_atcoder_3
+        filename = 'main.py'
+        code = 'print("hello")\n'
+        python_version = 'auto'
+        python_interpreter = 'cpython'
+        expected = ['3023']
+
+        language_dict = {language.id: language.name for language in languages}  # type: Dict[LanguageId, str]
+        self.assertEqual(submit.guess_lang_ids_of_file(filename=pathlib.Path(filename), code=code.encode(), language_dict=language_dict, python_version=python_version, python_interpreter=python_interpreter), expected)
+
+    def test_atcoder_3_pypy_2(self):
+        languages = languages_atcoder_3
+        filename = 'main.py'
+        code = textwrap.dedent("""\
+            #!/usr/bin/env python2
+            print("hello")
+            """)
+        python_version = 'auto'
+        python_interpreter = 'pypy'
+        expected = ['3509']
+
+        language_dict = {language.id: language.name for language in languages}  # type: Dict[LanguageId, str]
+        self.assertEqual(submit.guess_lang_ids_of_file(filename=pathlib.Path(filename), code=code.encode(), language_dict=language_dict, python_version=python_version, python_interpreter=python_interpreter), expected)
+
+    def test_atcoder_3_pypy_3(self):
+        languages = languages_atcoder_3
+        filename = 'main.py'
+        code = 'print("hello")\n'
+        python_version = 'auto'
+        python_interpreter = 'pypy'
+        expected = ['3510']
+
+        language_dict = {language.id: language.name for language in languages}  # type: Dict[LanguageId, str]
+        self.assertEqual(submit.guess_lang_ids_of_file(filename=pathlib.Path(filename), code=code.encode(), language_dict=language_dict, python_version=python_version, python_interpreter=python_interpreter), expected)
+
+    def test_atcoder_4_python_3(self):
+        languages = languages_atcoder_4
+        filename = 'main.py'
+        code = 'print("hello")\n'
+        python_version = 'auto'
+        python_interpreter = 'cpython'
+        expected = ['4006']
+
+        language_dict = {language.id: language.name for language in languages}  # type: Dict[LanguageId, str]
+        self.assertEqual(submit.guess_lang_ids_of_file(filename=pathlib.Path(filename), code=code.encode(), language_dict=language_dict, python_version=python_version, python_interpreter=python_interpreter), expected)
+
+    def test_atcoder_4_pypy_2(self):
+        languages = languages_atcoder_4
+        filename = 'main.py'
+        code = textwrap.dedent("""\
+            #!/usr/bin/env python2
+            print("hello")
+            """)
+        python_version = 'auto'
+        python_interpreter = 'pypy'
+        expected = ['4046']
+
+        language_dict = {language.id: language.name for language in languages}  # type: Dict[LanguageId, str]
+        self.assertEqual(submit.guess_lang_ids_of_file(filename=pathlib.Path(filename), code=code.encode(), language_dict=language_dict, python_version=python_version, python_interpreter=python_interpreter), expected)
+
+    def test_atcoder_4_pypy_3(self):
+        languages = languages_atcoder_4
+        filename = 'main.py'
+        code = 'print("hello")\n'
+        python_version = 'auto'
+        python_interpreter = 'pypy'
+        expected = ['4047']
+
+        language_dict = {language.id: language.name for language in languages}  # type: Dict[LanguageId, str]
+        self.assertEqual(submit.guess_lang_ids_of_file(filename=pathlib.Path(filename), code=code.encode(), language_dict=language_dict, python_version=python_version, python_interpreter=python_interpreter), expected)
+
+    def test_codeforces_python_2(self):
+        languages = languages_codeforces
+        filename = 'main.py'
+        code = textwrap.dedent("""\
+            #!/usr/bin/env python2
+            print("hello")
+            """)
+        python_version = 'auto'
+        python_interpreter = 'cpython'
+        expected = ['7']
+
+        language_dict = {language.id: language.name for language in languages}  # type: Dict[LanguageId, str]
+        self.assertEqual(submit.guess_lang_ids_of_file(filename=pathlib.Path(filename), code=code.encode(), language_dict=language_dict, python_version=python_version, python_interpreter=python_interpreter), expected)
+
+    def test_codeforces_python_3(self):
+        languages = languages_codeforces
+        filename = 'main.py'
+        code = 'print("hello")\n'
+        python_version = 'auto'
+        python_interpreter = 'cpython'
+        expected = ['31']
+
+        language_dict = {language.id: language.name for language in languages}  # type: Dict[LanguageId, str]
+        self.assertEqual(submit.guess_lang_ids_of_file(filename=pathlib.Path(filename), code=code.encode(), language_dict=language_dict, python_version=python_version, python_interpreter=python_interpreter), expected)
+
+    def test_codeforces_pypy_2(self):
+        languages = languages_codeforces
+        filename = 'main.py'
+        code = textwrap.dedent("""\
+            #!/usr/bin/env python2
+            print("hello")
+            """)
+        python_version = 'auto'
+        python_interpreter = 'pypy'
+        expected = ['40']
+
+        language_dict = {language.id: language.name for language in languages}  # type: Dict[LanguageId, str]
+        self.assertEqual(submit.guess_lang_ids_of_file(filename=pathlib.Path(filename), code=code.encode(), language_dict=language_dict, python_version=python_version, python_interpreter=python_interpreter), expected)
+
+    def test_codeforces_pypy_3(self):
+        languages = languages_codeforces
+        filename = 'main.py'
+        code = 'print("hello")\n'
+        python_version = 'auto'
+        python_interpreter = 'pypy'
+        expected = ['41']
+
+        language_dict = {language.id: language.name for language in languages}  # type: Dict[LanguageId, str]
+        self.assertEqual(submit.guess_lang_ids_of_file(filename=pathlib.Path(filename), code=code.encode(), language_dict=language_dict, python_version=python_version, python_interpreter=python_interpreter), expected)
+
+
+class LanguageGuessingOthersTest(unittest.TestCase):
+    def test_atcoder_3_rust(self):
+        languages = languages_atcoder_3
+        filename = 'main.rs'
+        code = 'fn main() { println!("Hello, world!"); }\n'
+        expected = ['3504']
+
+        language_dict = {language.id: language.name for language in languages}  # type: Dict[LanguageId, str]
+        self.assertEqual(submit.guess_lang_ids_of_file(filename=pathlib.Path(filename), code=code.encode(), language_dict=language_dict), expected)
+
+    def test_atcoder_4_fsharp(self):
+        languages = languages_atcoder_4
+        filename = 'Main.fs'
+        code = 'open System\nprintfn "Hello, world!"\n'
+        expected = ['4022', '4023']
+
+        language_dict = {language.id: language.name for language in languages}  # type: Dict[LanguageId, str]
+        self.assertEqual(submit.guess_lang_ids_of_file(filename=pathlib.Path(filename), code=code.encode(), language_dict=language_dict), expected)
+
+    def test_codeforces_haskell(self):
+        languages = languages_codeforces
+        filename = 'Main.hs'
+        code = 'main = return ()\n'
+        expected = ['12']
+
+        language_dict = {language.id: language.name for language in languages}  # type: Dict[LanguageId, str]
+        self.assertEqual(submit.guess_lang_ids_of_file(filename=pathlib.Path(filename), code=code.encode(), language_dict=language_dict), expected)