1# This file is part of the MicroPython project, http://micropython.org/
2#
3# The MIT License (MIT)
4#
5# Copyright (c) 2016 Rami Ali
6#
7# Permission is hereby granted, free of charge, to any person obtaining a copy
8# of this software and associated documentation files (the "Software"), to deal
9# in the Software without restriction, including without limitation the rights
10# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11# copies of the Software, and to permit persons to whom the Software is
12# furnished to do so, subject to the following conditions:
13#
14# The above copyright notice and this permission notice shall be included in
15# all copies or substantial portions of the Software.
16#
17# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23# THE SOFTWARE.
24
25""" gen-cpydiff generates documentation which outlines operations that differ between MicroPython
26    and CPython. This script is called by the docs Makefile for html and Latex and may be run
27    manually using the command make gen-cpydiff. """
28
29import os
30import errno
31import subprocess
32import time
33import re
34from collections import namedtuple
35
36# MicroPython supports syntax of CPython 3.4 with some features from 3.5, and
37# such version should be used to test for differences. If your default python3
38# executable is of lower version, you can point MICROPY_CPYTHON3 environment var
39# to the correct executable.
40if os.name == "nt":
41    CPYTHON3 = os.getenv("MICROPY_CPYTHON3", "python3.exe")
42    MICROPYTHON = os.getenv("MICROPY_MICROPYTHON", "../ports/windows/micropython.exe")
43else:
44    CPYTHON3 = os.getenv("MICROPY_CPYTHON3", "python3")
45    MICROPYTHON = os.getenv("MICROPY_MICROPYTHON", "../ports/unix/micropython")
46
47TESTPATH = "../tests/cpydiff/"
48DOCPATH = "../docs/genrst/"
49INDEXTEMPLATE = "../docs/differences/index_template.txt"
50INDEX = "index.rst"
51
52HEADER = ".. This document was generated by tools/gen-cpydiff.py\n\n"
53CLASSMAP = {"Core": "Core language", "Types": "Builtin types"}
54INDEXPRIORITY = ["syntax", "core_language", "builtin_types", "modules"]
55RSTCHARS = ["=", "-", "~", "`", ":"]
56SPLIT = '"""\n|categories: |description: |cause: |workaround: '
57TAB = "    "
58
59Output = namedtuple(
60    "output",
61    [
62        "name",
63        "class_",
64        "desc",
65        "cause",
66        "workaround",
67        "code",
68        "output_cpy",
69        "output_upy",
70        "status",
71    ],
72)
73
74
75def readfiles():
76    """Reads test files"""
77    tests = list(filter(lambda x: x.endswith(".py"), os.listdir(TESTPATH)))
78    tests.sort()
79    files = []
80
81    for test in tests:
82        text = open(TESTPATH + test, "r").read()
83
84        try:
85            class_, desc, cause, workaround, code = [
86                x.rstrip() for x in list(filter(None, re.split(SPLIT, text)))
87            ]
88            output = Output(test, class_, desc, cause, workaround, code, "", "", "")
89            files.append(output)
90        except IndexError:
91            print("Incorrect format in file " + TESTPATH + test)
92
93    return files
94
95
96def run_tests(tests):
97    """executes all tests"""
98    results = []
99    for test in tests:
100        with open(TESTPATH + test.name, "rb") as f:
101            input_py = f.read()
102
103        process = subprocess.Popen(
104            CPYTHON3,
105            shell=True,
106            stdout=subprocess.PIPE,
107            stdin=subprocess.PIPE,
108            stderr=subprocess.PIPE,
109        )
110        output_cpy = [com.decode("utf8") for com in process.communicate(input_py)]
111
112        process = subprocess.Popen(
113            MICROPYTHON,
114            shell=True,
115            stdout=subprocess.PIPE,
116            stdin=subprocess.PIPE,
117            stderr=subprocess.PIPE,
118        )
119        output_upy = [com.decode("utf8") for com in process.communicate(input_py)]
120
121        if output_cpy[0] == output_upy[0] and output_cpy[1] == output_upy[1]:
122            status = "Supported"
123            print("Supported operation!\nFile: " + TESTPATH + test.name)
124        else:
125            status = "Unsupported"
126
127        output = Output(
128            test.name,
129            test.class_,
130            test.desc,
131            test.cause,
132            test.workaround,
133            test.code,
134            output_cpy,
135            output_upy,
136            status,
137        )
138        results.append(output)
139
140    results.sort(key=lambda x: x.class_)
141    return results
142
143
144def indent(block, spaces):
145    """indents paragraphs of text for rst formatting"""
146    new_block = ""
147    for line in block.split("\n"):
148        new_block += spaces + line + "\n"
149    return new_block
150
151
152def gen_table(contents):
153    """creates a table given any set of columns"""
154    xlengths = []
155    ylengths = []
156    for column in contents:
157        col_len = 0
158        for entry in column:
159            lines = entry.split("\n")
160            for line in lines:
161                col_len = max(len(line) + 2, col_len)
162        xlengths.append(col_len)
163    for i in range(len(contents[0])):
164        ymax = 0
165        for j in range(len(contents)):
166            ymax = max(ymax, len(contents[j][i].split("\n")))
167        ylengths.append(ymax)
168
169    table_divider = "+" + "".join(["-" * i + "+" for i in xlengths]) + "\n"
170    table = table_divider
171    for i in range(len(ylengths)):
172        row = [column[i] for column in contents]
173        row = [entry + "\n" * (ylengths[i] - len(entry.split("\n"))) for entry in row]
174        row = [entry.split("\n") for entry in row]
175        for j in range(ylengths[i]):
176            k = 0
177            for entry in row:
178                width = xlengths[k]
179                table += "".join(["| {:{}}".format(entry[j], width - 1)])
180                k += 1
181            table += "|\n"
182        table += table_divider
183    return table + "\n"
184
185
186def gen_rst(results):
187    """creates restructured text documents to display tests"""
188
189    # make sure the destination directory exists
190    try:
191        os.mkdir(DOCPATH)
192    except OSError as e:
193        if e.args[0] != errno.EEXIST and e.args[0] != errno.EISDIR:
194            raise
195
196    toctree = []
197    class_ = []
198    for output in results:
199        section = output.class_.split(",")
200        for i in range(len(section)):
201            section[i] = section[i].rstrip()
202            if section[i] in CLASSMAP:
203                section[i] = CLASSMAP[section[i]]
204            if i >= len(class_) or section[i] != class_[i]:
205                if i == 0:
206                    filename = section[i].replace(" ", "_").lower()
207                    rst = open(DOCPATH + filename + ".rst", "w")
208                    rst.write(HEADER)
209                    rst.write(section[i] + "\n")
210                    rst.write(RSTCHARS[0] * len(section[i]))
211                    rst.write(time.strftime("\nGenerated %a %d %b %Y %X UTC\n\n", time.gmtime()))
212                    toctree.append(filename)
213                else:
214                    rst.write(section[i] + "\n")
215                    rst.write(RSTCHARS[min(i, len(RSTCHARS) - 1)] * len(section[i]))
216                    rst.write("\n\n")
217        class_ = section
218        rst.write(".. _cpydiff_%s:\n\n" % output.name.rsplit(".", 1)[0])
219        rst.write(output.desc + "\n")
220        rst.write("~" * len(output.desc) + "\n\n")
221        if output.cause != "Unknown":
222            rst.write("**Cause:** " + output.cause + "\n\n")
223        if output.workaround != "Unknown":
224            rst.write("**Workaround:** " + output.workaround + "\n\n")
225
226        rst.write("Sample code::\n\n" + indent(output.code, TAB) + "\n")
227        output_cpy = indent("".join(output.output_cpy[0:2]), TAB).rstrip()
228        output_cpy = ("::\n\n" if output_cpy != "" else "") + output_cpy
229        output_upy = indent("".join(output.output_upy[0:2]), TAB).rstrip()
230        output_upy = ("::\n\n" if output_upy != "" else "") + output_upy
231        table = gen_table([["CPy output:", output_cpy], ["uPy output:", output_upy]])
232        rst.write(table)
233
234    template = open(INDEXTEMPLATE, "r")
235    index = open(DOCPATH + INDEX, "w")
236    index.write(HEADER)
237    index.write(template.read())
238    for section in INDEXPRIORITY:
239        if section in toctree:
240            index.write(indent(section + ".rst", TAB))
241            toctree.remove(section)
242    for section in toctree:
243        index.write(indent(section + ".rst", TAB))
244
245
246def main():
247    """Main function"""
248
249    # set search path so that test scripts find the test modules (and no other ones)
250    os.environ["PYTHONPATH"] = TESTPATH
251    os.environ["MICROPYPATH"] = TESTPATH
252
253    files = readfiles()
254    results = run_tests(files)
255    gen_rst(results)
256
257
258main()
259