• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3#
4# Copyright (c) 2024 Huawei Device Co., Ltd.
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17
18import json
19import subprocess  # noqa: S404
20from dataclasses import dataclass
21from pathlib import Path
22from typing import Any, Literal, Protocol
23
24from pytest import fixture
25
26from arkdb.runnable_module import ScriptFile, syntax
27
28from .logs import logger
29
30LOG = logger(__name__)
31
32CompilerExtensions = Literal["js", "ts", "as", "sts"]
33
34CompileLogLevel = Literal["debug", "info", "warning", "error", "fatal"]
35
36
37class AstParser(Protocol):
38    def __call__(
39        self,
40        compiler_output: str,
41    ) -> dict[str, Any]:
42        pass
43
44
45@dataclass
46class Options:
47    """
48    Options for ``es2panda`` execution.
49    """
50
51    app_path: Path = Path("bin", "es2panda")
52    arktsconfig: Path = Path("bin", "arktsconfig.json")
53
54
55@dataclass
56class CompilerArguments:
57    extension: CompilerExtensions = "sts"
58    ets_module: bool = False
59    opt_level: int = 0
60    debug_info: bool = True
61    dump_dynamic_ast: bool | None = None
62    debugger_eval_mode: bool | None = None
63    debugger_eval_panda_files: list[Path] | None = None
64    debugger_eval_source: Path | None = None
65    debugger_eval_line: int | None = None
66    log_level: str | None = None
67
68    @staticmethod
69    def arg_to_str(arg: bool | list[Path] | Any) -> str:
70        if isinstance(arg, bool):
71            return str(arg).lower()
72        if isinstance(arg, list):
73            return ",".join(str(f) for f in arg)
74        return str(arg)
75
76    def to_arguments_list(self) -> list[str]:
77        result = []
78        for key, value in vars(self).items():
79            if value is not None:
80                result.append(f"--{key.replace('_', '-')}={CompilerArguments.arg_to_str(value)}")
81        return result
82
83
84@dataclass
85class EvaluateCompileExpressionArgs:
86    ets_expression: str | Path
87    eval_panda_files: list[Path] | None = None
88    eval_source: Path | None = None
89    eval_line: int | None = None
90    eval_log_level: CompileLogLevel | None = None
91    ast_parser: AstParser | None = None
92    name: str = "evaluated_expression"
93    extension: CompilerExtensions | None = None
94
95
96class CompileError(Exception):
97    def __init__(self, stdout: str) -> None:
98        super().__init__([stdout])
99        self.stdout = stdout
100
101
102class Compiler:
103    """
104    Controls the ``es2panda``.
105    """
106
107    def __init__(self, options: Options) -> None:
108        self.options = options
109
110    @staticmethod
111    def _compile(
112        args: list[str],
113        cwd: Path = Path(),
114    ) -> tuple[str, str]:
115        LOG.info("Compile %s", args)
116        proc = subprocess.run(  # noqa: S603
117            args=args,
118            cwd=cwd,
119            text=True,
120            capture_output=True,
121        )
122        if proc.returncode == 1:
123            raise CompileError(f"Stdout:\n{proc.stdout}\nStderr:\n{proc.stderr}")
124        proc.check_returncode()
125        return proc.stdout, proc.stderr
126
127    def compile(
128        self,
129        source_file: Path,
130        output_file: Path,
131        cwd: Path = Path(),
132        arguments: CompilerArguments | None = None,
133    ) -> tuple[str, str]:
134        args = [
135            str(self.options.app_path),
136            f"--arktsconfig={self.options.arktsconfig}",
137            f"--output={output_file}",
138        ]
139        opt_args = arguments if arguments is not None else CompilerArguments()
140        args += opt_args.to_arguments_list()
141        args.append(str(source_file))
142
143        return Compiler._compile(args=args, cwd=cwd)
144
145
146@fixture
147def ark_compiler(
148    ark_compiler_options: Options,
149) -> Compiler:
150    """
151    Return :class:`Compiler` instans that controls ``es2panda`` process.
152    """
153    return Compiler(ark_compiler_options)
154
155
156@fixture
157def ast_parser() -> AstParser:
158    def parser(compiler_output: str) -> dict[str, Any]:
159        return json.loads(compiler_output)
160
161    return parser
162
163
164def ark_compile(
165    source_file: Path,
166    tmp_path: Path,
167    ark_compiler: Compiler,
168    arguments: CompilerArguments | None = None,
169    ast_parser: AstParser | None = None,
170):
171    if not source_file.exists():
172        raise FileNotFoundError(source_file)
173    if not tmp_path.exists():
174        raise FileNotFoundError(tmp_path)
175
176    panda_file = tmp_path / f"{source_file.stem}.abc"
177    try:
178        stdout, stderr = ark_compiler.compile(
179            source_file=source_file,
180            output_file=panda_file,
181            cwd=tmp_path,
182            arguments=arguments,
183        )
184        if stderr:
185            LOG.debug("ES2PANDA STDERR:\n%s", stderr)
186
187    except CompileError as e:
188        LOG.error("Compile error", rich=syntax(source_file.read_text(), start_line=1))
189        if e.stdout:
190            LOG.error("STDOUT\n%s", e.stdout)
191        raise e
192    except subprocess.CalledProcessError as e:
193        LOG.error("Compiler failed with %s code", e.returncode, rich=syntax(source_file.read_text(), start_line=1))
194        if e.stdout:
195            LOG.error("STDOUT\n%s", e.stdout)
196        if e.stderr:
197            LOG.error("STDERR\n%s", e.stderr)
198        raise e
199
200    ast: dict[str, Any] | None = None
201    if ast_parser and arguments and arguments.dump_dynamic_ast:
202        ast = ast_parser(stdout)
203    return ScriptFile(source_file, panda_file, ast=ast)
204
205
206class StringCodeCompiler:
207
208    def __init__(self, tmp_path: Path, ark_compiler: Compiler) -> None:
209        self._tmp_path = tmp_path
210        self._ark_compiler = ark_compiler
211
212    def compile(
213        self,
214        source_code: str,
215        name: str = "test_string",
216        arguments: CompilerArguments | None = None,
217        ast_parser: AstParser | None = None,
218    ) -> ScriptFile:
219        """
220        Compiles the ``sts``-file and returns the :class:`ScriptFile` instance.
221        """
222        args = arguments if arguments else CompilerArguments()
223        source_file = self._write_into_file(source_code, name, args.extension)
224        return ark_compile(
225            source_file=source_file,
226            tmp_path=self._tmp_path,
227            ark_compiler=self._ark_compiler,
228            arguments=args,
229            ast_parser=ast_parser,
230        )
231
232    def compile_expression(
233        self,
234        eval_args: EvaluateCompileExpressionArgs,
235    ) -> ScriptFile:
236        args = CompilerArguments(
237            ets_module=True,
238            opt_level=0,
239            dump_dynamic_ast=ast_parser is not None,
240            debugger_eval_mode=True,
241            debugger_eval_panda_files=eval_args.eval_panda_files,
242            debugger_eval_source=eval_args.eval_source,
243            debugger_eval_line=eval_args.eval_line,
244            log_level=eval_args.eval_log_level if eval_args.eval_log_level is not None else "error",
245        )
246        if eval_args.extension is not None:
247            args.extension = eval_args.extension
248
249        ets_file: Path
250        if isinstance(eval_args.ets_expression, str):
251            ets_file = self._write_into_file(eval_args.ets_expression, eval_args.name, args.extension)
252        else:
253            ets_file = eval_args.ets_expression
254        if not ets_file.exists():
255            raise FileNotFoundError(ets_file)
256
257        return ark_compile(
258            ets_file,
259            self._tmp_path,
260            self._ark_compiler,
261            arguments=args,
262            ast_parser=eval_args.ast_parser,
263        )
264
265    def _write_into_file(self, source_code: str, filename: str, extension: str) -> Path:
266        ets_file = self._tmp_path / f"{filename}.{extension}"
267        ets_file.write_text(source_code)
268        return ets_file
269
270
271@fixture
272def code_compiler(
273    tmp_path: Path,
274    ark_compiler: Compiler,
275) -> StringCodeCompiler:
276    """
277    Return :class:`StringCodeCompiler` instance that can compile ``sts``-file.
278    """
279    return StringCodeCompiler(
280        tmp_path=tmp_path,
281        ark_compiler=ark_compiler,
282    )
283