diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3dabc4a4..410e48b52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,9 +9,15 @@ jobs: fail-fast: false matrix: include: - - php: '8.0' + - php: '8.2' moodle-branch: 'master' database: 'pgsql' + - php: '8.1' + moodle-branch: 'MOODLE_403_STABLE' + database: 'mariadb' + - php: '8.0' + moodle-branch: 'MOODLE_402_STABLE' + database: 'pgsql' - php: '7.4' moodle-branch: 'MOODLE_401_STABLE' database: 'pgsql' @@ -30,7 +36,7 @@ jobs: options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 3 mariadb: - image: mariadb:10.5 + image: mariadb:10.6 env: MYSQL_USER: 'root' MYSQL_ALLOW_EMPTY_PASSWORD: "true" @@ -65,6 +71,9 @@ jobs: - name: Start JobeInABox run: sudo docker run -d -p 4000:80 --name jobe trampgeek/jobeinabox:latest + - name: Let JobeInABox settle + run: sleep 5s + - name: Test JobeInABox run: | curl http://localhost:4000/jobe/index.php/restapi/languages @@ -88,6 +97,7 @@ jobs: MOODLE_BRANCH: ${{ matrix.moodle-branch }} - name: PHP Lint + continue-on-error: true # This step will show errors but will not fail if: ${{ always() }} run: moodle-plugin-ci phplint @@ -102,10 +112,12 @@ jobs: run: moodle-plugin-ci phpmd - name: Moodle Code Checker + continue-on-error: true # This step will show errors but will not fail if: ${{ always() }} run: moodle-plugin-ci codechecker --max-warnings 0 - name: Moodle PHPDoc Checker + continue-on-error: true # This step will show errors but will not fail if: ${{ always() }} run: moodle-plugin-ci phpdoc diff --git a/.gitignore b/.gitignore index 19b7e7e7c..67f0e8831 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ NonRepoFiles/* /amd/src/ui_blockly.js /amd/src/ui_blockly.json /amd/src/.eslintrc.js +.grunt + diff --git a/Readme.md b/Readme.md index 432905caa..e87d03ea7 100644 --- a/Readme.md +++ b/Readme.md @@ -1,6 +1,6 @@ # CodeRunner -Version: 5.1.1 9 November 2022. Requires **MOODLE V4.0 or later**. Earlier versions +Version: 5.2.2 18 September 2023. Requires **MOODLE V4.0 or later**. Earlier versions of Moodle must use CodeRunner V4. @@ -69,13 +69,26 @@ unusual question type. - [Extended column specifier syntax (*obsolescent*)](#extended-column-specifier-syntax-obsolescent) - [Default result columns](#default-result-columns) - [User-interface selection](#user-interface-selection) - - [The Graph UI](#the-graph-ui) - - [The Table UI](#the-table-ui) - - [The Gap Filler UI](#the-gap-filler-ui) - - [The Ace Gap Filler UI](#the-ace-gap-filler-ui) + - [Ace UI](#ace-ui) + - [Serialisation](#serialisation) + - [UI parameters](#ui-parameters) + - [Ace-gapfiller UI](#ace-gapfiller-ui) + - [Serialisation](#serialisation-1) + - [UI parameters](#ui-parameters-1) + - [Gap Filler UI](#gap-filler-ui) + - [Serialisation](#serialisation-2) + - [UI parameters](#ui-parameters-2) + - [Graph UI](#graph-ui) + - [Serialisation](#serialisation-3) + - [UI Parameters](#ui-parameters) - [The Html UI](#the-html-ui) + - [UI parameters](#ui-parameters-3) + - [Serialisation](#serialisation-4) - [The textareaId macro](#the-textareaid-macro) - - [Other UI plugins](#other-ui-plugins) + - [Scratchpad UI](#scratchpad-ui) + - [Serialisation](#serialisation-5) + - [UI parameters](#ui-parameters-4) + - [Table UI](#table-ui) - [User-defined question types](#user-defined-question-types) - [Prototype template parameters](#prototype-template-parameters) - [Supporting or implementing new languages](#supporting-or-implementing-new-languages) @@ -114,7 +127,7 @@ However, it is also possible to configure CodeRunner questions so that the mark is determined by how many of the tests the code successfully passed. CodeRunner has been in use at the University of Canterbury for over ten years -running many millions of student quiz question submissions in Python, C , JavaScript, +running many millions of student quiz question submissions in Python, C, JavaScript, PHP, Octave and Matlab. It is used in laboratory work, assignments, tests and exams in multiple courses. In recent years CodeRunner has spread around the world and as of January 2021 is installed on over 1800 Moodle sites worldwide @@ -361,7 +374,7 @@ the code or OS features within the Jobe container, e.g. to install new languages If you intend running unit tests you will also need to copy the file `tests/fixtures/test-sandbox-config-dist.php` -to 'tests/fixtures/test-sandbox-config.php', then edit it to set the correct +to `tests/fixtures/test-sandbox-config.php`, then edit it to set the correct host and any other necessary configuration for the Jobe server. Assuming you have built *Jobe* on a separate server, suitably firewalled, @@ -386,7 +399,7 @@ are installed. Before running any tests you first need to copy the file `/question/type/coderunner/tests/fixtures/test-sandbox-config-dist.php` -to '/question/type/coderunner/tests/fixtures/test-sandbox-config.php', +to `/question/type/coderunner/tests/fixtures/test-sandbox-config.php`, then edit it to set whatever configuration of sandboxes you wish to test, and to set the jobe host, if appropriate. You should then initialise the phpunit environment with the commands @@ -792,7 +805,7 @@ where a 'match' is defined by the chosen grader: an exact match, a nearly exact match or a regular-expression match. There is also the possibility to perform grading with the the template itself using a 'template grader'; this possibility is discussed later, in the section -'[Grading with templates'](#grading-with-templates). +[Grading with templates](#grading-with-templates). Expansion of the template is done by the [Twig](http://twig.sensiolabs.org/) template engine. The engine is given both @@ -1391,10 +1404,10 @@ student's first name: first_name = args['firstname'] print(json.dumps({'func_name': func_name, 'first_name': first_name})) -The question text could then say +The question text could then say: -Write a function {{ func_name }}() that prints a welcome message of the -form "Hello {{ first_name }}!". +`Write a function {{ func_name }}() that prints a welcome message of the +form "Hello {{ first_name }}!".` Note that this simple example is chosen only to illustrate the technique. It is a very bad example of *when* to use @@ -1489,9 +1502,9 @@ they are not available as global variables. Other fields are: * `QUESTION.name` The name of the question. - * `QUESTION.generalfeedback The contents of the general feedback field in the + * `QUESTION.generalfeedback` The contents of the general feedback field in the question authoring form. - * `QUESTION.generalfeedbackformat. The format of the general feedback. 0 = moodle, + * `QUESTION.generalfeedbackformat` The format of the general feedback. 0 = moodle, 1 = HTML, 2 = Plain, 3 = Wiki, 4 = Markdown. * `QUESTION.questiontext` The question text itself * `QUESTION.answerpreload` The string that is preloaded into the answer box. @@ -2039,11 +2052,11 @@ A template grader for this situation might be the following else: comment += "Line {} wrong\n".format(i) - print(json.dumps({'got': got, 'comment': comment, 'fraction': mark / 5, 'awarded': mark})) + print(json.dumps({'got': got, 'comment': comment, 'fraction': mark / 5})) Note that in the above program the Python *dictionary* - {'got': got, 'comment': comment, 'fraction': mark / 5, 'awarded': mark} + {'got': got, 'comment': comment, 'fraction': mark / 5} gets converted by the call to json.dumps to a JSON object string, which looks syntactically similar but is in fact a different sort of entity altogether. @@ -2051,12 +2064,19 @@ You should always use json.dumps, or its equivalent in other languages, to generate a valid JSON string, handling details like escaping of embedded newlines. -In order to display the *comment* and *awarded* columns in the output JSON, +In order to display the *comment* column in the output JSON, the 'Result columns' field of the question (in the 'customisation' part of -the question authoring form) should include those field and their column headers, e.g. +the question authoring form) should include that field and its column header, e.g. [["Expected", "expected"], ["Got", "got"], ["Comment", "comment"], ["Mark", "awarded"]] +Note that the 'awarded' value, which is what is displayed in the 'Mark' column, +is by default computed as the product of the +faction and the number of marks allocated to the particular test case. You can +alternatively include an 'awarded' attribute in the JSON but this is not +generally recommended; if you do this, make sure that you award a mark in the +range 0 to the number of marks allocated to the test case. + The following two images show the student's result table after submitting a fully correct answer and a partially correct answer, respectively. @@ -2070,8 +2090,9 @@ usual graders (e.g. exact or regular-expression matching of the program's output) are inadequate. As a simple example, suppose the student has to write their own Python square -root function (perhaps as an exercise in Newton-Raphson iteration?), such -that their answer, when squared, is within an absolute tolerance of 0.000001 +root function (perhaps as an exercise in Newton-Raphson iteration?), which is +to be named *my_sqrt*. Their function is required to return an answer that +is within an absolute tolerance of 0.000001 of the correct answer. To prevent them from using the math module, any use of an import statement would need to be disallowed but we'll ignore that aspect in order to focus on the grading aspect. @@ -2079,7 +2100,7 @@ in order to focus on the grading aspect. The simplest way to deal with this issue is to write a series of testcases of the form - approx = student_sqrt(2) + approx = my_sqrt(2) right_answer = math.sqrt(2) if math.abs(approx - right_answer) < 0.00001: print("OK") @@ -2089,7 +2110,7 @@ of the form where the expected output is "OK". However, if one wishes to test the student's code with a large number of values - say 100 or more - this approach becomes impracticable. For that, we need to write our own tester, which we can do -using a template grade. +using a template grader. Template graders that run student-supplied code are somewhat tricky to write correctly, as they need to output a valid JSON record under all situations, @@ -2098,7 +2119,7 @@ errors or syntax error. The safest approach is usually to run the student's code in a subprocess and then grade the output. A per-test template grader for the student square root question, which tests -the student's *student_sqrt* function with 1000 random numbers in the range +the student's *my_sqrt* function with 1000 random numbers in the range 0 to 1000, might be as follows: import subprocess, json, sys @@ -2118,7 +2139,7 @@ the student's *student_sqrt* function with 1000 random numbers in the range ok = True for i in range(NUM_TESTS): x = uniform(0, 1000) - stud_answer = student_sqrt(n) + stud_answer = my_sqrt(n) right = math.sqrt(x) if abs(right - stud_answer) > TOLERANCE: print("Wrong sqrt for {}. Expected {}, got {}".format(x, right, stud_answer)) @@ -2262,8 +2283,9 @@ unchecking a *Use Ace* checkbox, but this disabled it both for student answers and for the author's template. Since version 3.3.0, CodeRunner now supports pluggable user interfaces, -although an administrator has to install the plugin. The two user interfaces -currently built in to CodeRunner are Ace and Graph. The question author selects the required +although an administrator has to install the plugin. The user interfaces +currently built in to CodeRunner are Ace, Ace-gapfiller, Gapfiller, Graph, +Scratchpad and Table. The question author selects the required user interface via a dropdown menu in the customisation section of the question author form. The selection controls editing of the sample answer and answer preload fields of the authoring form and the student's answer in the live @@ -2271,144 +2293,102 @@ quiz. The Ace editor is always used for editing the template itself, unless turned off with the *Template uses ace* checkbox in the authoring form. -### The Graph UI +The value of the STUDENT_ANSWER variable seen by the template code is different +for the various UI plugins. For example, with the Ace editor the STUDENT_ANSWER +is simply the raw text edited by Ace, while for the Ace-gapfiller it's a JSON +list of the string values that the student entered into the gaps. -The Graph UI plugin -provides simple graph-drawing capabilities to support -questions where the student is asked to draw or edit a graph. By default the -Graph UI, which was developed for Finite State Machines, draws directed graphs, -allows nodes to be marked as *Accept* states and allows incoming start edges. -For example: +Most UI plugins support a few configuration options via a UI parameters entry +field in the question authoring form. - +All active CodeRunner user interface plugins in both the question authoring +form and the student's quiz page can be toggled off and on with a +CTRL-ALT-M keypress, alternately exposing and hiding the underlying textarea element. -Clicking the Help button on the graph canvas displays information on how to -draw graphs. +The general behaviour, serialisation and UI parameters of the supported plugins +are as follows. -Some limited control of the Graph UI is available to the question author -via template parameters as follows: +### Ace UI - 1. isdirected - defaults to true. Set it to false for a non-directed graph. +This is the default UI interface and is the one most-commonly used for programming +questions. - 1. isfsm - defaults to false. Set it to true to allow edges to enter the -graph from space, i.e., without a start node. It also allows nodes to be marked -as accept states by double clicking. +#### Serialisation +The serialisation is simply the raw text that the Ace editor is displaying. - 1. noderadius - defaults to 26. The radius of nodes, in pixels. +#### UI parameters +Mostly the default configuration options will be used but a few +specialised UI parameters exist: - 1. fontsize - defaults to 20. The size of the Arial font, in px. + 1. auto_switch_light_dark. If true, this parameter allows a browser or OS + colour-scheme preference for a dark theme to override the default Ace + theme setting for a question. Default: false. - 1. textoffset. An offset in pixels used when positioning link label text. - Default 4. + 1. font_size. The font-size for the Ace editor. Default: 14 px. - 1. locknodes. True to prevent the user from moving nodes. Useful when the -answer box is preloaded with a graph that the student has to annotate by -changing node or edge labels or by adding/removing edges. Note, though that -nodes can still be added and deleted. + 1. import_from_scratchpad. True to allow the Ace editor to detect that a + question appears to have been configured for the scratchpad UI, and + extract the actual code from the JSON. Should not be changed from its + default of true unless you want students to edit JSON objects with an + 'answer_code' key. Default: true. - 1. lockedges. True to prevent the user from dragging edges to change -their curvature. Possibly useful if the -answer box is preloaded with a graph that the student has to annotate by -changing node or edge labels or by adding/removing edges. Also ensures that -edges added by a student are straight, e.g. to draw a polygon on a set of -given points. Note, though that edges can still be added and deleted. + 1. live_autocompletion. Turns on the Ace editor auto-complete function. - 1. helpmenutext - text to replace the default help menu text. Must be a - single JSON string written on line using "\n" to separate lines in the menu. - For example: + 1. theme. The theme to be used by the ace editor. Type ctrl + ',' within + the Ace editor to see a list of available themes. Default: textmate. - {"helpmenutext": "Line1\nLine2\nLine3"} +If a user uses the ctrl + ',' option to select a theme, this theme will be used +in all Ace editor windows within that browser until changed back. - The default value, written here in multiple lines for readability, is: - - Double click at a blank space to create a new node/state. - - Double click an existing node to "mark" it e.g. as an accept state for Finite State Machines - (FSMs). Double click again to unmark it. - - Click and drag to move a node. - - Alt click (or Ctrl alt click) and drag on a node to move a (sub)graph. - - Shift click inside one node and drag to another to create a link. - - Shift click on a blank space, drag to a node to create a start link (FSMs only). - - Click and drag a link to alter its curve. - - Click on a link/node to edit its text. - - Typing _ followed by a digit makes that digit a subscript. - - Typing \\epsilon creates an epsilon character (and similarly for \\alpha, \\beta etc). - - Click on a link/node then press the Delete key to remove it (or function-delete on a Mac). +### Ace-gapfiller UI +A UI that presents the user with an Ace editor window containing code with some +gaps in it. The user is expected to fill in the gaps. Only simple gaps at most +one line in length are supported. -For example, for a non-directed non-fsm graph set the template parameters field to +The text to be displayed in the editor window is by default the contents of the +globalextra field in the question author form, but can alternatively be set +from the code in the first test case (see the ui_source UI parameter below). +The text will normally be most of a program but with one or more bits replaced by a +gap specifier of the form - {"isdirected": false, "isfsm": false} + {[20-40]} -or merge those values into any other template parameters required by the -question. +where the two numbers are the default field width and maximum field width +respectively. It the second number (and the preceding '-') is omitted, +the field width can expand arbitrarily. -Other template parameters may be added as required by specific questions. +For example (a case in which the source is test0): -Many thanks to Emily Price for the original implementation of the Graph UI. + -All active CodeRunner user interface plugins in both the question authoring -form and the student's quiz page can be toggled off and on with a -CTRL-ALT-M keypress, alternately exposing and hiding the underlying textarea element. +#### Serialisation -### The Table UI +The serialisation is a +JSON list of strings, which are the values entered by the student into the +gaps. -The table UI plug-in replaces the usual textarea answer element with an HTML table, -into which the student must enter text data. All cells in the table are -HTML *textarea* elements. The question author can enable *Add row* and -*Delete row* buttons that allow the student to add or delete rows. The configuration -of the table is set by the following template parameters, where the first two -are required and the rest are optional. +#### UI parameters - * `num_rows` (required): sets the (initial) number of table rows, excluding the header. - * `num_columns` (required): sets the number of table columns. - * `column_headers` (optional): a list of strings used for column headers. By default - no column headers are used. - * `row_labels` (optional): a list of strings used for row labels. By - default no row labels are used. - * `lines_per_cell` (optional): the initial number of rows for each of the - table text-area cells. Default 2. - * `column_width_percents` (optional): a list of numeric percentage widths of the different - columns. For example, if there are two columns, and the first one is to - occupy one-quarter of the available width, the list should be \[25, 75\]. - By default all columns have the same width. - * `dynamic_rows` (optional): set `true` to enable the addition of *Add row* - and *Delete row* buttons through which the student can alter the number of - rows. The number of rows can never be less than the initial `num_rows` value. - * `locked_cells` (optional): an array of 2-element [row, column] cell specifiers. - The specified cells are rendered to HTML with the *disabled* attribute, so - cannot be changed by the user. For example - - "locked_cells": [[0, 0], [1, 0]] - - to lock the leftmost column of rows 0 and 1. - This is primarily for use in conjunction with - an answer preload in which some cells are defined by the question author. - The preload answer must be defined before the locked_cells template - parameter is defined, or the question author will not be able to define - the required values in the first place. + 1. ui_source. This parameter specifies where to get the source text + to be displayed in the editor (with gaps as specified above). + The default value is "globalextra" but the alternative is "test0". + In the latter case, the contents of the test code field of the first + test is used. In this latter case, all other test cases should contain + corresponding gap fillers and the result table will substitute the + student's gap fillers into all tests with syntax colouring to denote + the substitution. In this mode, you can't use the "Use as example" + feature because the test code isn't defined until the student has + filled in the gaps. -For example, the `python3\_program\_testing` question type uses the following -UI parameter setting: +### Gap Filler UI - { - "num_rows": 3, - "num_columns": 2, - "column_headers": ["Test", "Result"], - "dynamic_rows": true - } +This plugin is an older version of the Ace gapfiller UI and has largely been +superseded by it. It does have one advantage over the Ace gapfiller: it +allows for multiline HTML text area gaps as well as single line HTML input +elements. But the program is displayed as simple non-syntax-coloured text. - The table serialisation is simply a JSON array of arrays containing all the -table cells excluding the header row. - -As a special case of the serialisation, if all values in the serialisation -are empty strings, the serialisation is -itself the empty string. - -An example of the use of this UI type can be seen in the -*python3_program_testing* prototype in the *samples* folder. - -### The Gap Filler UI - -This plugin replaces the usual textarea answer box with a div +This UI replaces the usual textarea answer box with a div consisting of pre-formatted text supplied by the question author in either the "globalextra" field or the testcode field of the first test case, according to the ui parameter ui_source (default: globalextra). HTML @@ -2434,6 +2414,8 @@ where size, rows and column are integer literals. These respectively inject an HTML input element or a textarea element of the specified size. +#### Serialisation + The serialisation of the answer box contents, i.e. the text that is copied back into the textarea for submission as the answer, is simply a list of all the field values (strings), in order. @@ -2441,30 +2423,122 @@ as the answer, is simply a list of all the field values (strings), in order. As a special case of the serialisation, if the value list is empty, the serialisation itself is the empty string. -The delimiters for the input element insertion tags are by default '{[' and -']}', but can be changed by an optional UI parameter delimiters, -which must be a 2-element array of strings. For example: +#### UI parameters + + 1. ui_source. As with the Ace gapfiller, this sets the source for the program + source with the inserted gaps. It can be set to "globalextra" to take + the HTML from the globalextra field or to "test0" to take if from the + test code of the first test case. + + 1. delimiters. A 2-element array of the strings used to open and close the gap + description. Default ["{[", "]}"] + + 1. sync_interval_secs. The time interval in seconds between calls to sync the + UI contents back to the question answer. 0 for no such auto-syncing. + + +### Graph UI + +The Graph UI plugin +provides simple graph-drawing capabilities to support +questions where the student is asked to draw or edit a graph. By default the +Graph UI, which was developed for Finite State Machines, draws directed graphs, +allows nodes to be marked as *Accept* states and allows incoming start edges. +For example: + + + +Clicking the Help button on the graph canvas displays information on how to +draw graphs. + +#### Serialisation + +The serialised Graph UI STUDENT_ANSWER is a JSON object with the following attributes: + + 1. nodes. An array of 2-element arrays [nodelabel, is_acceptor]. The 'is_acceptor' + value is a boolean that's true for accept state nodes in FSM graphs, + false otherwise. - {"delimiters": ["{{", "}}"]} + 1. edges. An array of 3-element arrays [from_node_num, to_node_num, edge_label]. + Node numbers are indices into the nodes array (0-origin). -Note that the double-brace delimiters in that example are the same as those -used by Twig, so using them instead of the default would prevent you from -ever adding Twig expansion (e.g. for randomisation) to the question. This is -not recommended. + 1. nodeGeometry. An array of 2-element arrays that are the coordinates of + the nodes. + 1. edgeGeometry. An array of JSON objects, that define the shape of the + in-general-circular arcs connecting two nodes. Hopefully you never need + to understand this attribute. -### The Ace Gap Filler UI +#### UI Parameters -This is a variant on the Gap Filler UI in which -the text is rendered by the Ace editor with all usual syntax highlighting -but the user can edit only the text in the gaps. +Some limited control of the Graph UI is available to the question author +via template parameters as follows: + + 1. isdirected - defaults to true. Set it to false for a non-directed graph. + + 1. isfsm - defaults to false. Set it to true to allow edges to enter the +graph from space, i.e., without a start node. It also allows nodes to be marked +as accept states by double clicking. + + 1. noderadius - defaults to 26. The radius of nodes, in pixels. + + 1. fontsize - defaults to 20. The size of the Arial font, in px. + + 1. textoffset. An offset in pixels used when positioning link label text. + Default 4. + + 1. locknodepositions. True to prevent the user from moving nodes. Useful when the +answer box is preloaded with a graph that the student has to annotate by +changing node or edge labels or by adding/removing edges. Note, though that +nodes can still be added and deleted. See locknodeset. Default false. + + 1. locknodelabels. True to prevent the user from editing node labels. Also + prevents any new nodes having non-empty labels. Default false. + + 1. locknodeset. True to prevent user from adding or deleting nodes or toggling + node types to/from acceptors. Default false. + + 1. lockedgepositions. True to prevent the user from dragging edges to change + their curvature. Possibly useful if the + answer box is preloaded with a graph that the student has to annotate by + changing node or edge labels or by adding/removing edges. Also ensures that + edges added by a student are straight, e.g. to draw a polygon on a set of + given points. Note, though that edges can still be added and deleted. See lockedgeset. + Default false. + + 1. lockedgelabels. True to prevent the user from editing edge labels. Also + prevents any new edges from having labels. Default false. + + 1. lockedgeset. True to prevent the user from adding or deleting edges. + Default false. + + 1. helpmenutext - text to replace the default help menu text. Must be a + single JSON string written on line using "\n" to separate lines in the menu. + For example: + + {"helpmenutext": "Line1\nLine2\nLine3"} + + The default value, written here in multiple lines for readability, is: + + - Double click at a blank space to create a new node/state. + - Double click an existing node to "mark" it e.g. as an accept state for Finite State Machines + (FSMs). Double click again to unmark it. + - Click and drag to move a node. + - Alt click (or Ctrl alt click) and drag on a node to move a (sub)graph. + - Shift click inside one node and drag to another to create a link. + - Shift click on a blank space, drag to a node to create a start link (FSMs only). + - Click and drag a link to alter its curve. + - Click on a link/node to edit its text. + - Typing _ followed by a digit makes that digit a subscript. + - Typing \\epsilon creates an epsilon character (and similarly for \\alpha, \\beta etc). + - Click on a link/node then press the Delete key to remove it (or function-delete on a Mac). + +For example, for a non-directed non-fsm graph set the UI parameters field to + + {"isdirected": false, "isfsm": false} + +Many thanks to Emily Price for the original implementation of the Graph UI. -It behaves exactly like the Gap Filler UI, above, except that it does not -currently support the {[ rows, columns ]} syntax for multiline gaps. Only -in-line gaps are supported. In addition, the field width can have a maximum -width set, with a syntax like {[20-40]}, meaning the initial field width -is 20 characters but can expand up to 40. If the maximum value is omitted, the -field can expand to an arbitrary width. ### The Html UI @@ -2472,47 +2546,79 @@ The Html UI plug-in replaces the answer box with custom HTML provided by the question author. The HTML will usually include data entry fields such as html input and text area elements and it is the values that the user enters into these fields that constitutes the student answer. The HTML can -also include JavaScript in ` QEND - , 'format' => FORMAT_HTML); - return $form; + , 'format' => FORMAT_HTML]; + return $form; } @@ -654,13 +671,13 @@ public function get_coderunner_question_form_data_demows() { */ public function make_coderunner_question_sqr_user_prototype_child() { $coderunner = $this->make_coderunner_question( - 'sqr_user_prototype', - 'Program to test prototype', - 'Answer should (somehow) produce the expected answer below', - array( - array('expected' => "This is data\nLine 2") - ), - array('templateparams' => '{"xxx":1, "zzz":2}') + 'sqr_user_prototype', + 'Program to test prototype', + 'Answer should (somehow) produce the expected answer below', + [ + ['expected' => "This is data\nLine 2"], + ], + ['templateparams' => '{"xxx":1, "zzz":2}'] ); return $coderunner; } @@ -674,19 +691,20 @@ public function make_coderunner_question_sqr_user_prototype_child() { */ public function make_coderunner_question_sqr_c() { $coderunner = $this->make_coderunner_question( - 'c_function', - 'Function to square a number n', - 'Write a function int sqr(int n) that returns n squared.', - array( - array('testcode' => 'printf("%d", sqr(0));', - 'expected' => '0'), - array('testcode' => 'printf("%d", sqr(7));', - 'expected' => '49'), - array('testcode' => 'printf("%d", sqr(-11));', - 'expected' => '121'), - array('testcode' => 'printf("%d", sqr(-16));', - 'expected' => '256') - )); + 'c_function', + 'Function to square a number n', + 'Write a function int sqr(int n) that returns n squared.', + [ + ['testcode' => 'printf("%d", sqr(0));', + 'expected' => '0'], + ['testcode' => 'printf("%d", sqr(7));', + 'expected' => '49'], + ['testcode' => 'printf("%d", sqr(-11));', + 'expected' => '121'], + ['testcode' => 'printf("%d", sqr(-16));', + 'expected' => '256'], + ] + ); return $coderunner; } @@ -701,13 +719,14 @@ public function make_coderunner_question_sqr_c() { */ public function make_coderunner_question_sqr_c_single_test() { $coderunner = $this->make_coderunner_question( - 'c_function', - 'Function to square a number n', - 'Write a function int sqr(int n) that returns n squared.', - array( - array('testcode' => 'printf("%d", sqr(-11)); fflush(stdout);', - 'expected' => '121') - )); + 'c_function', + 'Function to square a number n', + 'Write a function int sqr(int n) that returns n squared.', + [ + ['testcode' => 'printf("%d", sqr(-11)); fflush(stdout);', + 'expected' => '121'], + ] + ); return $coderunner; } @@ -719,19 +738,20 @@ public function make_coderunner_question_sqr_c_single_test() { */ public function make_coderunner_question_sqr_no_semicolons() { $coderunner = $this->make_coderunner_question( - 'c_function', - 'Function to square a number n', - 'Write a function int sqr(int n) that returns n squared.', - array( - array('testcode' => 'printf("%d", sqr(0))', - 'expected' => '0'), - array('testcode' => 'printf("%d", sqr(7))', - 'expected' => '49'), - array('testcode' => 'printf("%d", sqr(-11))', - 'expected' => '121'), - array('testcode' => 'printf("%d", sqr(-16))', - 'expected' => '256') - )); + 'c_function', + 'Function to square a number n', + 'Write a function int sqr(int n) that returns n squared.', + [ + ['testcode' => 'printf("%d", sqr(0))', + 'expected' => '0'], + ['testcode' => 'printf("%d", sqr(7))', + 'expected' => '49'], + ['testcode' => 'printf("%d", sqr(-11))', + 'expected' => '121'], + ['testcode' => 'printf("%d", sqr(-16))', + 'expected' => '256'], + ] + ); return $coderunner; } @@ -742,13 +762,14 @@ public function make_coderunner_question_sqr_no_semicolons() { */ public function make_coderunner_question_hello_prog_c() { $coderunner = $this->make_coderunner_question( - 'c_program', - 'Program to print "Hello ENCE260"', - 'Write a program that prints "Hello ENCE260"', - array( - array('testcode' => '', - 'expected' => 'Hello ENCE260') - )); + 'c_program', + 'Program to print "Hello ENCE260"', + 'Write a program that prints "Hello ENCE260"', + [ + ['testcode' => '', + 'expected' => 'Hello ENCE260'], + ] + ); return $coderunner; } @@ -760,17 +781,18 @@ public function make_coderunner_question_hello_prog_c() { */ public function make_coderunner_question_copy_stdin_c() { $coderunner = $this->make_coderunner_question( - 'c_program', - 'Function to copy n lines of stdin to stdout', - 'Write a function copyLines(n) that reads stdin to stdout', - array( - array('stdin' => '', - 'expected' => ''), - array('stdin' => "Line1\n", - 'expected' => "Line1\n"), - array('stdin' => "Line1\nLine2\n", - 'expected' => "Line1\nLine2\n") - )); + 'c_program', + 'Function to copy n lines of stdin to stdout', + 'Write a function copyLines(n) that reads stdin to stdout', + [ + ['stdin' => '', + 'expected' => ''], + ['stdin' => "Line1\n", + 'expected' => "Line1\n"], + ['stdin' => "Line1\nLine2\n", + 'expected' => "Line1\nLine2\n"], + ] + ); return $coderunner; } @@ -778,23 +800,24 @@ public function make_coderunner_question_copy_stdin_c() { public function make_coderunner_question_str_to_upper() { $coderunner = $this->make_coderunner_question( - 'c_function', - 'Function to convert string to uppercase', - 'Write a function void str_to_upper(char s[]) that converts s to uppercase', - array( - array('testcode' => " + 'c_function', + 'Function to convert string to uppercase', + 'Write a function void str_to_upper(char s[]) that converts s to uppercase', + [ + ['testcode' => " char s[] = {'1','@','a','B','c','d','E',';', 0}; str_to_upper(s); printf(\"%s\\n\", s); ", - 'expected' => '1@ABCDE;'), - array('testcode' => " + 'expected' => '1@ABCDE;'], + ['testcode' => " char s[] = {'1','@','A','b','C','D','e',';', 0}; str_to_upper(s); printf(\"%s\\n\", s); ", - 'expected' => '1@ABCDE;') - )); + 'expected' => '1@ABCDE;'], + ] + ); return $coderunner; } @@ -809,19 +832,20 @@ public function make_coderunner_question_str_to_upper() { */ public function make_coderunner_question_string_delete() { $coderunner = $this->make_coderunner_question( - 'c_function', - 'Function to delete from a source string all chars present in another string', - 'Write a function void string_delete(char *s, const char *charsToDelete) '. + 'c_function', + 'Function to delete from a source string all chars present in another string', + 'Write a function void string_delete(char *s, const char *charsToDelete) ' . 'that takes any two C strings as parameters and modifies the ' . 'string s by deleting from it all characters that are present in charsToDelete.', - array( - array('testcode' => "char s[] = \"abcdefg\";\nstring_delete(s, \"xcaye\");\nprintf(\"%s\\n\", s);", - 'expected' => 'bdfg'), - array('testcode' => "char s[] = \"abcdefg\";\nstring_delete(s, \"\");\nprintf(\"%s\\n\", s);", - 'expected' => 'abcdefg'), - array('testcode' => "char s[] = \"aaaaabbbbb\";\nstring_delete(s, \"x\");\nprintf(\"%s\\n\", s);", - 'expected' => 'aaaaabbbbb') - )); + [ + ['testcode' => "char s[] = \"abcdefg\";\nstring_delete(s, \"xcaye\");\nprintf(\"%s\\n\", s);", + 'expected' => 'bdfg'], + ['testcode' => "char s[] = \"abcdefg\";\nstring_delete(s, \"\");\nprintf(\"%s\\n\", s);", + 'expected' => 'abcdefg'], + ['testcode' => "char s[] = \"aaaaabbbbb\";\nstring_delete(s, \"x\");\nprintf(\"%s\\n\", s);", + 'expected' => 'aaaaabbbbb'], + ] + ); return $coderunner; } @@ -835,19 +859,20 @@ public function make_coderunner_question_string_delete() { */ public function make_coderunner_question_sqr_cpp() { $coderunner = $this->make_coderunner_question( - 'cpp_function', - 'Function to square a number n', - 'Write a function int sqr(int n) that returns n squared.', - array( - array('testcode' => 'cout << sqr(0);', - 'expected' => '0'), - array('testcode' => 'cout << sqr(7);', - 'expected' => '49'), - array('testcode' => 'cout << sqr(-11);', - 'expected' => '121'), - array('testcode' => 'cout << sqr(-16);', - 'expected' => '256') - )); + 'cpp_function', + 'Function to square a number n', + 'Write a function int sqr(int n) that returns n squared.', + [ + ['testcode' => 'cout << sqr(0);', + 'expected' => '0'], + ['testcode' => 'cout << sqr(7);', + 'expected' => '49'], + ['testcode' => 'cout << sqr(-11);', + 'expected' => '121'], + ['testcode' => 'cout << sqr(-16);', + 'expected' => '256'], + ] + ); return $coderunner; } @@ -860,13 +885,14 @@ public function make_coderunner_question_sqr_cpp() { */ public function make_coderunner_question_hello_prog_cpp() { $coderunner = $this->make_coderunner_question( - 'cpp_program', - 'Program to print "Hello ENCE260"', - 'Write a program that prints "Hello ENCE260"', - array( - array('testcode' => '', - 'expected' => 'Hello ENCE260') - )); + 'cpp_program', + 'Program to print "Hello ENCE260"', + 'Write a program that prints "Hello ENCE260"', + [ + ['testcode' => '', + 'expected' => 'Hello ENCE260'], + ] + ); return $coderunner; } @@ -878,17 +904,18 @@ public function make_coderunner_question_hello_prog_cpp() { */ public function make_coderunner_question_copy_stdin_cpp() { $coderunner = $this->make_coderunner_question( - 'cpp_program', - 'Program to copies stdin to stdout', - 'Write a program that reads stdin to stdout', - array( - array('stdin' => '', - 'expected' => ''), - array('stdin' => "Line1\n", - 'expected' => "Line1\n"), - array('stdin' => "Line1\nLine2\n", - 'expected' => "Line1\nLine2\n") - )); + 'cpp_program', + 'Program to copies stdin to stdout', + 'Write a program that reads stdin to stdout', + [ + ['stdin' => '', + 'expected' => ''], + ['stdin' => "Line1\n", + 'expected' => "Line1\n"], + ['stdin' => "Line1\nLine2\n", + 'expected' => "Line1\nLine2\n"], + ] + ); return $coderunner; } @@ -896,22 +923,23 @@ public function make_coderunner_question_copy_stdin_cpp() { public function make_coderunner_question_str_to_upper_cpp() { $coderunner = $this->make_coderunner_question( - 'cpp_function', - 'Function to convert string to uppercase', - 'Write a function str_to_upper(string s) that converts s to uppercase' + 'cpp_function', + 'Function to convert string to uppercase', + 'Write a function str_to_upper(string s) that converts s to uppercase' . 'and returns the ', - array( - array('testcode' => " + [ + ['testcode' => " string s = \"1@aBcdE;\"; cout << str_to_upper(s); ", - 'expected' => '1@ABCDE;'), - array('testcode' => " + 'expected' => '1@ABCDE;'], + ['testcode' => " string s = \"1@aBcDe;\"; cout << str_to_upper(s); ", - 'expected' => '1@ABCDE;') - )); + 'expected' => '1@ABCDE;'], + ] + ); return $coderunner; } @@ -929,16 +957,17 @@ public function make_coderunner_question_sqrmatlab() { 'matlab_function', 'Function to square a number n', 'Write a function sqr(n) that returns n squared.', - array( - array('testcode' => 'disp(sqr(0));', - 'expected' => ' 0'), - array('testcode' => 'disp(sqr(7));', - 'expected' => ' 49'), - array('testcode' => 'disp(sqr(-11));', - 'expected' => ' 121'), - array('testcode' => 'disp(sqr(-16));', - 'expected' => ' 256') - )); + [ + ['testcode' => 'disp(sqr(0));', + 'expected' => ' 0'], + ['testcode' => 'disp(sqr(7));', + 'expected' => ' 49'], + ['testcode' => 'disp(sqr(-11));', + 'expected' => ' 121'], + ['testcode' => 'disp(sqr(-16));', + 'expected' => ' 256'], + ] + ); return $coderunner; } @@ -953,7 +982,7 @@ public function make_coderunner_question_teststudentanswermacro() { } private function make_macro_question($qtype) { - $options = array(); + $options = []; $options['template'] = << 'mytest();', - 'expected' => "\"Hi!\" he said\n'Hi!' he said"), - array('testcode' => 'disp(ESCAPED_STUDENT_ANSWER);', + [ + ['testcode' => 'mytest();', + 'expected' => "\"Hi!\" he said\n'Hi!' he said"], + ['testcode' => 'disp(ESCAPED_STUDENT_ANSWER);', 'expected' => << 'disp(sqr(0));', - 'expected' => '0'), - array('testcode' => 'disp(sqr(7));', - 'expected' => '49'), - array('testcode' => 'disp(sqr(-11));', - 'expected' => '121'), - array('testcode' => 'disp(sqr(-16));', - 'expected' => '256') - )); + [ + ['testcode' => 'disp(sqr(0));', + 'expected' => '0'], + ['testcode' => 'disp(sqr(7));', + 'expected' => '49'], + ['testcode' => 'disp(sqr(-11));', + 'expected' => '121'], + ['testcode' => 'disp(sqr(-16));', + 'expected' => '256'], + ] + ); return $coderunner; } @@ -1036,16 +1069,17 @@ public function make_coderunner_question_sqrnodejs() { 'nodejs', 'Function to square a number n', 'Write a js function sqr(n) that returns n squared.', - array( - array('testcode' => 'console.log(sqr(0));', - 'expected' => '0'), - array('testcode' => 'console.log(sqr(7));', - 'expected' => '49'), - array('testcode' => 'console.log(sqr(-11));', - 'expected' => '121'), - array('testcode' => 'console.log(sqr(-16));', - 'expected' => '256') - )); + [ + ['testcode' => 'console.log(sqr(0));', + 'expected' => '0'], + ['testcode' => 'console.log(sqr(7));', + 'expected' => '49'], + ['testcode' => 'console.log(sqr(-11));', + 'expected' => '121'], + ['testcode' => 'console.log(sqr(-16));', + 'expected' => '256'], + ] + ); return $coderunner; } @@ -1063,19 +1097,20 @@ public function make_coderunner_question_teststudentanswermacrooctave() { */ public function make_coderunner_question_sqrjava() { $coderunner = $this->make_coderunner_question( - 'java_method', - 'Method to square a number n', - 'Write a method int sqr(int n) that returns n squared.', - array( - array('testcode' => 'System.out.println(sqr(0))', - 'expected' => '0'), - array('testcode' => 'System.out.println(sqr(7))', - 'expected' => '49'), - array('testcode' => 'System.out.println(sqr(-11))', - 'expected' => '121'), - array('testcode' => 'System.out.println(sqr(16))', - 'expected' => '256') - )); + 'java_method', + 'Method to square a number n', + 'Write a method int sqr(int n) that returns n squared.', + [ + ['testcode' => 'System.out.println(sqr(0))', + 'expected' => '0'], + ['testcode' => 'System.out.println(sqr(7))', + 'expected' => '49'], + ['testcode' => 'System.out.println(sqr(-11))', + 'expected' => '121'], + ['testcode' => 'System.out.println(sqr(16))', + 'expected' => '256'], + ] + ); return $coderunner; } @@ -1086,17 +1121,18 @@ public function make_coderunner_question_sqrjava() { */ public function make_coderunner_question_nameclass() { $coderunner = $this->make_coderunner_question( - 'java_class', - 'Name class', - 'Write a class Name with a constructor ' . + 'java_class', + 'Name class', + 'Write a class Name with a constructor ' . 'that has firstName and lastName parameters with a toString ' . 'method that returns firstName space lastName', - array( - array('testcode' => 'System.out.println(new Name("Joe", "Brown"))', - 'expected' => 'Joe Brown'), - array('testcode' => 'System.out.println(new Name("a", "b"))', - 'expected' => 'a b') - )); + [ + ['testcode' => 'System.out.println(new Name("Joe", "Brown"))', + 'expected' => 'Joe Brown'], + ['testcode' => 'System.out.println(new Name("a", "b"))', + 'expected' => 'a b'], + ] + ); return $coderunner; } @@ -1108,16 +1144,17 @@ public function make_coderunner_question_nameclass() { */ public function make_coderunner_question_printsquares() { $coderunner = $this->make_coderunner_question( - 'java_program', - 'Name class', - 'Write a program squares that reads an integer from stdin and prints ' . + 'java_program', + 'Name class', + 'Write a program squares that reads an integer from stdin and prints ' . 'the squares of all integers from 1 up to that number, all on one line, space separated.', - array( - array('stdin' => "5\n", - 'expected' => "1 4 9 16 25\n"), - array('stdin' => "1\n", - 'expected' => "1\n") - )); + [ + ['stdin' => "5\n", + 'expected' => "1 4 9 16 25\n"], + ['stdin' => "1\n", + 'expected' => "1\n"], + ] + ); return $coderunner; } @@ -1126,15 +1163,15 @@ public function make_coderunner_question_printsquares() { */ public function make_coderunner_question_multilang_echo_stdin() { return $this->make_coderunner_question( - 'multilanguage', - 'Multilang Echo', - 'Write a program in your language of choice to echo stdin to stdout', - array( - array( + 'multilanguage', + 'Multilang Echo', + 'Write a program in your language of choice to echo stdin to stdout', + [ + [ 'stdin' => "Line1\nLine2", - 'expected' => "Line1\nLine2") - ) - ); + 'expected' => "Line1\nLine2"], + ] + ); } /** @@ -1160,16 +1197,16 @@ public function make_coderunner_question_printstr() { } EOPROG; $q = $this->make_coderunner_question( - 'java_program', - 'Print string', - 'No question answer required', - array( - array('testcode' => $code, + 'java_program', + 'Print string', + 'No question answer required', + [ + ['testcode' => $code, 'stdin' => "5\n", - 'expected' => "a0\nb\t\nc\f\nd'This is a string'\n\"So is this\"") - ), - array('template' => $template, - 'iscombinatortemplate' => false) + 'expected' => "a0\nb\t\nc\f\nd'This is a string'\n\"So is this\""], + ], + ['template' => $template, + 'iscombinatortemplate' => false] ); return $q; } @@ -1182,19 +1219,20 @@ public function make_coderunner_question_printstr() { */ public function make_coderunner_question_sqrphp() { $coderunner = $this->make_coderunner_question( - 'php', - 'Function to square a number n', - 'Write a function sqr($n) that returns $n squared.', - array( - array('testcode' => 'print(sqr(0))', - 'expected' => '0'), - array('testcode' => 'print(sqr(7))', - 'expected' => '49'), - array('testcode' => 'print(sqr(-11))', - 'expected' => '121'), - array('testcode' => 'print(sqr(16))', - 'expected' => '256') - )); + 'php', + 'Function to square a number n', + 'Write a function sqr($n) that returns $n squared.', + [ + ['testcode' => 'print(sqr(0))', + 'expected' => '0'], + ['testcode' => 'print(sqr(7))', + 'expected' => '49'], + ['testcode' => 'print(sqr(-11))', + 'expected' => '121'], + ['testcode' => 'print(sqr(16))', + 'expected' => '256'], + ] + ); return $coderunner; } @@ -1217,13 +1255,16 @@ public function make_coderunner_question_sqrphp() { * field). */ private function get_options(&$question) { - global $CFG, $DB; + global $DB; $type = $question->coderunnertype; $questiontype = new qtype_coderunner(); - if (!$row = $DB->get_record_select( - 'question_coderunner_options', - "coderunnertype = '$type' and prototypetype != 0")) { + if ( + !$row = $DB->get_record_select( + 'question_coderunner_options', + "coderunnertype = '$type' and prototypetype != 0" + ) + ) { $error = "TestHelper: failed to load type info for question with type $type"; throw new qtype_coderunner_missing_question_type($error); } @@ -1244,7 +1285,7 @@ private function get_options(&$question) { } } - $question->options->answers = array(); // For compatability with questiontype base. + $question->options->answers = []; // For compatability with questiontype base. $question->options->testcases = $question->testcases; } @@ -1252,7 +1293,7 @@ private function get_options(&$question) { // of info, add in all the other necessary fields to get an array of // testcase objects. private static function make_test_cases($rawtests) { - $basictest = array('testtype' => 0, + $basictest = ['testtype' => 0, 'testcode' => '', 'stdin' => '', 'extra' => '', @@ -1260,8 +1301,8 @@ private static function make_test_cases($rawtests) { 'display' => 'SHOW', 'mark' => 1.0, 'hiderestiffail' => 0, - 'useasexample' => 0); - $tests = array(); + 'useasexample' => 0]; + $tests = []; foreach ($rawtests as $test) { $t = $basictest; // Copy. foreach ($test as $key => $value) { @@ -1275,8 +1316,13 @@ private static function make_test_cases($rawtests) { // Return a CodeRunner question of a given (sub)type with given testcases // and other options. Further fields might be added by // coderunnertestcase::make_question (q.v.). - private function make_coderunner_question($type, $name, $questiontext, - $testcases, $otheroptions = array()) { + private function make_coderunner_question( + $type, + $name, + $questiontext, + $testcases, + $otheroptions = [] + ) { question_bank::load_question_definition_classes('coderunner'); $coderunner = new qtype_coderunner_question(); test_question_maker::initialise_a_question($coderunner); @@ -1288,6 +1334,7 @@ private function make_coderunner_question($type, $name, $questiontext, $coderunner->templateparamsevald = null; $coderunner->uiparameters = null; $coderunner->hoisttemplateparams = 0; + $coderunner->extractcodefromjson = 1; $coderunner->twigall = 0; $coderunner->prototypetype = 0; $coderunner->name = $name; @@ -1305,7 +1352,7 @@ private function make_coderunner_question($type, $name, $questiontext, $coderunner->generalfeedback = 'No feedback available for coderunner questions.'; $coderunner->penaltyregime = '10, 20, ...'; $coderunner->testcases = self::make_test_cases($testcases); - $coderunner->options = array(); + $coderunner->options = []; $coderunner->isnew = true; // Extra field normally added by save_question. $coderunner->context = context_system::instance(); // Use system context for testing. $coderunner->attachments = 0; @@ -1314,7 +1361,7 @@ private function make_coderunner_question($type, $name, $questiontext, $coderunner->filenamesregex = ''; $coderunner->filenamesexplain = ''; $coderunner->prototypeextra = ''; - $coderunner->parameters = array(); // Normally generated during question initialisation. + $coderunner->parameters = []; // Normally generated during question initialisation. foreach ($otheroptions as $key => $value) { $coderunner->$key = $value; $coderunner->options[$key] = $value; diff --git a/tests/ideonesandbox_test.php b/tests/ideonesandbox_test.php index ec54ac36f..e1de7af95 100644 --- a/tests/ideonesandbox_test.php +++ b/tests/ideonesandbox_test.php @@ -33,8 +33,10 @@ global $CFG; require_once($CFG->dirroot . '/question/type/coderunner/tests/test.php'); +/** + * @coversNothing + */ class ideonesandbox_test extends \qtype_coderunner_testcase { - public function test_testfunction() { $this->check_sandbox_enabled('ideonesandbox'); $sandbox = new \qtype_coderunner_ideonesandbox(); // Lots happens here! @@ -138,5 +140,3 @@ public function test_ideone_sandbox_ok_c() { $sandbox->close(); } } - - diff --git a/tests/java_question_test.php b/tests/java_question_test.php index 93e351e6f..16770df08 100644 --- a/tests/java_question_test.php +++ b/tests/java_question_test.php @@ -35,13 +35,20 @@ /** * Unit tests for coderunner Java questions + * @coversNothing */ class java_question_test extends \qtype_coderunner_testcase { + protected function setUp(): void { + parent::setUp(); + + // Each test will be skipped if java not available on jobe server. + $this->check_language_available('java'); + } public function test_good_sqr_function() { $q = $this->make_question('sqrjava'); - $response = array('answer' => "int sqr(int n) { return n * n; }\n"); - list($mark, $grade, $cache) = $q->grade_response($response); + $response = ['answer' => "int sqr(int n) { return n * n; }\n"]; + [$mark, $grade, $cache] = $q->grade_response($response); $this->assertEquals(1, $mark); $this->assertEquals(\question_state::$gradedright, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -53,8 +60,8 @@ public function test_good_sqr_function() { public function test_bad_sqr_function() { $q = $this->make_question('sqrjava'); - $response = array('answer' => "int sqr(int n) { return n; }\n"); - list($mark, $grade, $cache) = $q->grade_response($response); + $response = ['answer' => "int sqr(int n) { return n; }\n"]; + [$mark, $grade, $cache] = $q->grade_response($response); $this->assertEquals(0, $mark); $this->assertEquals(\question_state::$gradedwrong, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -66,8 +73,8 @@ public function test_bad_sqr_function() { public function test_bad_syntax() { $q = $this->make_question('sqrjava'); - $response = array('answer' => "int sqr(n) { return n * n; }\n"); - list($mark, $grade, $cache) = $q->grade_response($response); + $response = ['answer' => "int sqr(n) { return n * n; }\n"]; + [$mark, $grade, $cache] = $q->grade_response($response); $this->assertEquals(0, $mark); $this->assertEquals(\question_state::$gradedwrong, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -79,7 +86,7 @@ public function test_bad_syntax() { public function test_class_type() { $q = $this->make_question('nameclass'); - $response = array('answer' => << <<grade_response($response); +, + ]; + [$mark, $grade, $cache] = $q->grade_response($response); $this->assertEquals(1, $mark); $this->assertEquals(\question_state::$gradedright, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -104,7 +112,7 @@ class Name { public function test_program_type() { $q = $this->make_question('printsquares'); - $response = array('answer' => << <<grade_response($response); +, + ]; + [$mark, $grade, $cache] = $q->grade_response($response); $this->assertEquals(1, $mark); $this->assertEquals(\question_state::$gradedright, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -130,7 +139,7 @@ public function test_program_type() { public function test_program_type_alternate_syntax() { $q = $this->make_question('printsquares'); - $response = array('answer' => << <<grade_response($response); +, + ]; + [$mark, $grade, $cache] = $q->grade_response($response); $this->assertEquals(1, $mark); $this->assertEquals(\question_state::$gradedright, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -158,8 +168,8 @@ public function test_program_type_alternate_syntax() { // Checks if the Java Twig escape filter works. public function test_java_escape() { $q = $this->make_question('printstr'); - $response = array('answer' => ''); - list($mark, $grade, $cache) = $q->grade_response($response); + $response = ['answer' => '']; + [$mark, , ] = $q->grade_response($response); $this->assertEquals(1, $mark); } -} \ No newline at end of file +} diff --git a/tests/jobesandbox_test.php b/tests/jobesandbox_test.php index 851d2b1dc..5d098cea4 100644 --- a/tests/jobesandbox_test.php +++ b/tests/jobesandbox_test.php @@ -38,9 +38,10 @@ global $CFG; require_once($CFG->dirroot . '/question/type/coderunner/tests/test.php'); - +/** + * @coversNothing + */ class jobesandbox_test extends \qtype_coderunner_testcase { - public function test_fail_with_bad_key() { $this->check_sandbox_enabled('jobesandbox'); if (!get_config('qtype_coderunner', 'jobe_apikey_enabled')) { @@ -104,9 +105,13 @@ public function test_jobesandbox_python3_with_files() { print(open('second.bb').read()) "; $sandbox = new \qtype_coderunner_jobesandbox(); - $result = $sandbox->execute($source, 'python3', '', - array('first.a' => "Line1\nLine2", - 'second.bb' => 'Otherfile')); + $result = $sandbox->execute( + $source, + 'python3', + '', + ['first.a' => "Line1\nLine2", + 'second.bb' => 'Otherfile'] + ); $this->assertEquals(\qtype_coderunner_sandbox::OK, $result->error); $this->assertEquals(\qtype_coderunner_sandbox::RESULT_SUCCESS, $result->result); $this->assertEquals('', $result->stderr); @@ -195,7 +200,7 @@ public function test_limits_enforced() { $sandbox = new \qtype_coderunner_jobesandbox(); $source = 'print("Hello sandbox!")'; for ($i = 0; $i < $maxnumtries; $i++) { - $result = $sandbox->execute($source, 'python3', '', null, array('debug' => 1)); + $result = $sandbox->execute($source, 'python3', '', null, ['debug' => 1]); if ($result->error === $sandbox::SUBMISSION_LIMIT_EXCEEDED) { return; } else { diff --git a/tests/matlab_question_test.php b/tests/matlab_question_test.php index cbb283b0b..f89fbf38c 100644 --- a/tests/matlab_question_test.php +++ b/tests/matlab_question_test.php @@ -37,14 +37,21 @@ /** * Unit tests for coderunner matlab questions + * @coversNothing */ class matlab_question_test extends \qtype_coderunner_testcase { + protected function setUp(): void { + parent::setUp(); + + // Each test will be skipped if matlab not available on jobe server. + $this->check_language_available('matlab'); + } public function test_good_sqr_function() { $this->check_language_available('matlab'); $q = $this->make_question('sqrmatlab'); - $response = array('answer' => "function sq = sqr(n)\n sq = n * n;\nend\n"); - list($mark, $grade, $cache) = $q->grade_response($response); + $response = ['answer' => "function sq = sqr(n)\n sq = n * n;\nend\n"]; + [$mark, $grade, $cache] = $q->grade_response($response); $this->assertEquals(1, $mark); $this->assertEquals(\question_state::$gradedright, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -57,8 +64,8 @@ public function test_good_sqr_function() { public function test_bad_sqr_function() { $this->check_language_available('matlab'); $q = $this->make_question('sqrmatlab'); - $response = array('answer' => "function sq = sqr(n)\n sq = n;\nend\n"); - list($mark, $grade, $cache) = $q->grade_response($response); + $response = ['answer' => "function sq = sqr(n)\n sq = n;\nend\n"]; + [$mark, $grade, $cache] = $q->grade_response($response); $this->assertEquals(0, $mark); $this->assertEquals(\question_state::$gradedwrong, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -71,8 +78,8 @@ public function test_bad_sqr_function() { public function test_bad_syntax() { $this->check_language_available('matlab'); $q = $this->make_question('sqrmatlab'); - $response = array('answer' => "function sq = sqr(n)\n sq = n;\nendd\n"); - list($mark, $grade, $cache) = $q->grade_response($response); + $response = ['answer' => "function sq = sqr(n)\n sq = n;\nendd\n"]; + [$mark, $grade, $cache] = $q->grade_response($response); $this->assertEquals(0, $mark); $this->assertEquals(\question_state::$gradedwrong, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -84,7 +91,7 @@ public function test_bad_syntax() { public function test_student_answer_macro() { $this->check_language_available('matlab'); $q = $this->make_question('teststudentanswermacro'); - $response = array('answer' => << <<grade_response($response); +, + ]; + [$mark, $grade, $cache] = $q->grade_response($response); $this->assertEquals(1, $mark); $this->assertEquals(\question_state::$gradedright, $grade); } - } diff --git a/tests/nodejs_question_test.php b/tests/nodejs_question_test.php index 80c348ae1..86ae5e95e 100644 --- a/tests/nodejs_question_test.php +++ b/tests/nodejs_question_test.php @@ -36,14 +36,21 @@ /** * Unit tests for coderunner nodejs questions. + * @coversNothing */ class nodejs_question_test extends \qtype_coderunner_testcase { + protected function setUp(): void { + parent::setUp(); + + // Each test will be skipped if nodejs not available on jobe server. + $this->check_language_available('nodejs'); + } public function test_good_sqr_function() { $this->check_language_available('nodejs'); $q = $this->make_question('sqrnodejs'); - $response = array('answer' => "function sqr(n) {\n return n * n;\n}\n"); - list($mark, $grade, $cache) = $q->grade_response($response); + $response = ['answer' => "function sqr(n) {\n return n * n;\n}\n"]; + [$mark, $grade, $cache] = $q->grade_response($response); $this->assertEquals(1, $mark); $this->assertEquals(\question_state::$gradedright, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -57,8 +64,8 @@ public function test_bad_sqr_function() { // Return a wrong answer in this version. $this->check_language_available('nodejs'); $q = $this->make_question('sqrnodejs'); - $response = array('answer' => "function sqr(n) {\n return n\n}\n"); - list($mark, $grade, $cache) = $q->grade_response($response); + $response = ['answer' => "function sqr(n) {\n return n\n}\n"]; + [$mark, $grade, $cache] = $q->grade_response($response); $this->assertEquals(0, $mark); $this->assertEquals(\question_state::$gradedwrong, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -66,4 +73,3 @@ public function test_bad_sqr_function() { $this->assertFalse($testoutcome->all_correct()); } } - diff --git a/tests/octave_question_test.php b/tests/octave_question_test.php index 8781c9387..3d133162a 100644 --- a/tests/octave_question_test.php +++ b/tests/octave_question_test.php @@ -36,14 +36,21 @@ /** * Unit tests for coderunner octave questions. + * @coversNothing */ class octave_question_test extends \qtype_coderunner_testcase { + protected function setUp(): void { + parent::setUp(); - public function test_good_sqr_function() { + // Each test will be skipped if octave not available on jobe server. $this->check_language_available('octave'); + } + + + public function test_good_sqr_function() { $q = $this->make_question('sqroctave'); - $response = array('answer' => "function sq = sqr(n)\n sq = n * n;\nend\n"); - list($mark, $grade, $cache) = $q->grade_response($response); + $response = ['answer' => "function sq = sqr(n)\n sq = n * n;\nend\n"]; + [$mark, $grade, $cache] = $q->grade_response($response); $this->assertEquals(1, $mark); $this->assertEquals(\question_state::$gradedright, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -54,10 +61,9 @@ public function test_good_sqr_function() { public function test_bad_sqr_function() { - $this->check_language_available('octave'); $q = $this->make_question('sqroctave'); - $response = array('answer' => "function sq = sqr(n)\n sq = n;\nend\n"); - list($mark, $grade, $cache) = $q->grade_response($response); + $response = ['answer' => "function sq = sqr(n)\n sq = n;\nend\n"]; + [$mark, $grade, $cache] = $q->grade_response($response); $this->assertEquals(0, $mark); $this->assertEquals(\question_state::$gradedwrong, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -68,10 +74,9 @@ public function test_bad_sqr_function() { public function test_bad_syntax() { - $this->check_language_available('octave'); $q = $this->make_question('sqroctave'); - $response = array('answer' => "function sq = sqr(n)\n sq = n:\nend\n"); - list($mark, $grade, $cache) = $q->grade_response($response); + $response = ['answer' => "function sq = sqr(n)\n sq = n:\nend\n"]; + [$mark, $grade, $cache] = $q->grade_response($response); $this->assertEquals(0, $mark); $this->assertEquals(\question_state::$gradedwrong, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -82,9 +87,8 @@ public function test_bad_syntax() { } public function test_student_answer_macro() { - $this->check_language_available('octave'); $q = $this->make_question('teststudentanswermacrooctave'); - $response = array('answer' => << <<grade_response($response); + [$mark, $grade, ] = $q->grade_response($response); $this->assertEquals(1, $mark); $this->assertEquals(\question_state::$gradedright, $grade); } diff --git a/tests/penaltyregime_test.php b/tests/penaltyregime_test.php index 1f924d65e..2883a0023 100644 --- a/tests/penaltyregime_test.php +++ b/tests/penaltyregime_test.php @@ -34,21 +34,20 @@ require_once($CFG->dirroot . '/question/type/coderunner/tests/helper.php'); /** - * Unit tests for the coderunner question type. + * More extensive testing of penalty regime. * + * @coversNothing * @copyright 2011 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -/** More extensive testing of penalty regime. - */ - class penaltyregime_test extends \qbehaviour_walkthrough_test_base { - protected function setUp(): void { - global $CFG; parent::setUp(); \qtype_coderunner_testcase::setup_test_sandbox_configuration(); + if (!get_config('qtype_coderunner', 'jobesandbox_enabled')) { + $this->markTestSkipped("Jobe sandbox unavailable: test skipped"); + } } // Support function to run the sqr question with the given penalty regime, @@ -56,16 +55,16 @@ protected function setUp(): void { // Check the resulting mark = $expected. public function run_with_regime($regime, $numbadattempts, $expected) { $helper = new \qtype_coderunner_test_helper(); - $q = $helper->make_coderunner_question_sqr(array('penaltyregime' => $regime)); + $q = $helper->make_coderunner_question_sqr(['penaltyregime' => $regime]); $this->start_attempt_at_question($q, 'adaptive', 1, 1); for ($i = 1; $i <= $numbadattempts; $i++) { // Submit a totally wrong answer $numbadattempts times. $badanswer = 'def sqr(n): return ' . (-100 * $i); - $this->process_submission(array('-submit' => 1, 'answer' => $badanswer)); + $this->process_submission(['-submit' => 1, 'answer' => $badanswer]); $this->check_current_mark(0.0); } // Now get it right. - $this->process_submission(array('-submit' => 1, 'answer' => 'def sqr(n): return n * n')); + $this->process_submission(['-submit' => 1, 'answer' => 'def sqr(n): return n * n']); $this->check_current_mark($expected); } diff --git a/tests/phpquestions_test.php b/tests/phpquestions_test.php index 561c3a828..dbe3506f2 100644 --- a/tests/phpquestions_test.php +++ b/tests/phpquestions_test.php @@ -34,15 +34,22 @@ /** * Unit tests for the coderunner question definition class. This file tests - * a simple PHP question + * a simple PHP question. + * @coversNothing */ class phpquestions_test extends \qtype_coderunner_testcase { + protected function setUp(): void { + parent::setUp(); - public function test_good_sqr_function() { + // Each test will be skipped if php not available on jobe server. $this->check_language_available('php'); + } + + + public function test_good_sqr_function() { $q = $this->make_question('sqrphp'); - $response = array('answer' => "grade_response($response); + $response = ['answer' => "grade_response($response); $this->assertEquals(1, $mark); $this->assertEquals(\question_state::$gradedright, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -53,10 +60,9 @@ public function test_good_sqr_function() { public function test_bad_sqr_function() { - $this->check_language_available('php'); $q = $this->make_question('sqrphp'); - $response = array('answer' => "grade_response($response); + $response = ['answer' => "grade_response($response); $this->assertEquals(0, $mark); $this->assertEquals(\question_state::$gradedwrong, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -67,10 +73,9 @@ public function test_bad_sqr_function() { public function test_bad_syntax() { - $this->check_language_available('php'); $q = $this->make_question('sqrphp'); - $response = array('answer' => "grade_response($response); + $response = ['answer' => "grade_response($response); $this->assertEquals(0, $mark); $this->assertEquals(\question_state::$gradedwrong, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -79,4 +84,3 @@ public function test_bad_syntax() { $this->assertEquals(0, count($testoutcome->testresults)); } } - diff --git a/tests/precheckwalkthrough_test.php b/tests/precheckwalkthrough_test.php index 0edde66c6..2c48bfce8 100644 --- a/tests/precheckwalkthrough_test.php +++ b/tests/precheckwalkthrough_test.php @@ -35,18 +35,22 @@ use qtype_coderunner\constants; +/** + * @coversNothing + */ class precheckwalkthrough_test extends \qbehaviour_walkthrough_test_base { - protected function setUp(): void { - global $CFG; parent::setUp(); \qtype_coderunner_testcase::setup_test_sandbox_configuration(); + if (!get_config('qtype_coderunner', 'jobesandbox_enabled')) { + $this->markTestSkipped("Jobe sandbox unavailable: test skipped"); + } } protected function make_precheck_question() { $q = \test_question_maker::make_question('coderunner', 'sqr'); - $q->testcases = array( - (object) array('testtype' => 2, // Both. + $q->testcases = [ + (object) ['testtype' => 2, // Both. 'testcode' => 'print(sqr(-11))', 'expected' => '121', 'stdin' => '', @@ -54,8 +58,8 @@ protected function make_precheck_question() { 'display' => 'SHOW', 'extra' => '', 'mark' => 1.0, - 'hiderestiffail' => 0), - (object) array('testtype' => 2, // Both. + 'hiderestiffail' => 0], + (object) ['testtype' => 2, // Both. 'testcode' => 'print(sqr(12))', 'expected' => '144', 'stdin' => '', @@ -63,8 +67,8 @@ protected function make_precheck_question() { 'display' => 'SHOW', 'extra' => '', 'mark' => 1.0, - 'hiderestiffail' => 0), - (object) array('testtype' => 0, // Normal (i.e. not precheck). + 'hiderestiffail' => 0], + (object) ['testtype' => 0, // Normal (i.e. not precheck). 'testcode' => 'print(sqr(-7))', 'expected' => '49', 'stdin' => '', @@ -72,8 +76,8 @@ protected function make_precheck_question() { 'display' => 'SHOW', 'extra' => '', 'mark' => 1.0, - 'hiderestiffail' => 0), - (object) array('testtype' => 1, // Precheck only. + 'hiderestiffail' => 0], + (object) ['testtype' => 1, // Precheck only. 'testcode' => 'print(sqr(-5))', 'expected' => '25', 'stdin' => '', @@ -81,8 +85,8 @@ protected function make_precheck_question() { 'display' => 'SHOW', 'extra' => '', 'mark' => 1.0, - 'hiderestiffail' => 0) - ); + 'hiderestiffail' => 0], + ]; $q->template = <<assertEquals('Not complete', $qa->get_state_string(true)); // Precheck with a wrong answer. - $this->process_submission(array('-precheck' => 1, 'answer' => "def sqr(n): return n\n")); + $this->process_submission(['-precheck' => 1, 'answer' => "def sqr(n): return n\n"]); $this->check_output_contains("Precheck only"); $this->check_output_contains('print(sqr(-11))'); $this->check_output_contains('print(sqr(12))'); @@ -117,7 +121,7 @@ public function test_precheck_examples() { $this->assertEquals("Prechecked: def sqr(n): return n\n", $qa->summarise_action($qa->get_last_step())); // Now re-precheck with a right answer. - $this->process_submission(array('-precheck' => 1, 'answer' => "def sqr(n): return n * n\n")); + $this->process_submission(['-precheck' => 1, 'answer' => "def sqr(n): return n * n\n"]); $this->check_output_contains("Precheck only"); $this->check_output_contains('print(sqr(-11))'); $this->check_output_contains('print(sqr(12))'); @@ -130,7 +134,7 @@ public function test_precheck_examples() { $this->save_quba(); // Now click check with a wrong answer. - $this->process_submission(array('-submit' => 1, 'answer' => "def sqr(n): return n\n")); + $this->process_submission(['-submit' => 1, 'answer' => "def sqr(n): return n\n"]); $this->check_output_does_not_contain("Precheck only"); $this->check_output_contains('print(sqr(-11))'); $this->check_output_contains('print(sqr(12))'); @@ -142,7 +146,7 @@ public function test_precheck_examples() { $this->check_current_mark(0.0); // Lastly check with a right answer, verify that a single 20% penalty was incurred. - $this->process_submission(array('-submit' => 1, 'answer' => "def sqr(n): return n * n\n")); + $this->process_submission(['-submit' => 1, 'answer' => "def sqr(n): return n * n\n"]); $this->check_output_does_not_contain("Precheck only"); $this->check_output_contains('print(sqr(-11))'); $this->check_output_contains('print(sqr(12))'); @@ -164,7 +168,7 @@ public function test_precheck_selected() { $this->start_attempt_at_question($q, 'adaptive', 1, 1); // Precheck with a wrong answer. - $this->process_submission(array('-precheck' => 1, 'answer' => "def sqr(n): return n\n")); + $this->process_submission(['-precheck' => 1, 'answer' => "def sqr(n): return n\n"]); $this->check_output_contains("Precheck only"); $this->check_output_contains('print(sqr(-11))'); $this->check_output_contains('print(sqr(12))'); @@ -176,7 +180,7 @@ public function test_precheck_selected() { $this->check_current_mark(null); // Now re-precheck with a right answer. - $this->process_submission(array('-precheck' => 1, 'answer' => "def sqr(n): return n * n\n")); + $this->process_submission(['-precheck' => 1, 'answer' => "def sqr(n): return n * n\n"]); $this->check_output_contains('print(sqr(-11))'); $this->check_output_contains('print(sqr(12))'); $this->check_output_does_not_contain('print(sqr(-7))'); @@ -187,7 +191,7 @@ public function test_precheck_selected() { $this->save_quba(); // Now click check with a wrong answer. - $this->process_submission(array('-submit' => 1, 'answer' => "def sqr(n): return n\n")); + $this->process_submission(['-submit' => 1, 'answer' => "def sqr(n): return n\n"]); $this->check_output_contains('print(sqr(-11))'); $this->check_output_contains('print(sqr(12))'); $this->check_output_contains('print(sqr(-7))'); @@ -197,7 +201,7 @@ public function test_precheck_selected() { $this->check_current_mark(0.0); // Lastly check with a right answer, verify that a single 20% penalty was incurred. - $this->process_submission(array('-submit' => 1, 'answer' => "def sqr(n): return n * n\n")); + $this->process_submission(['-submit' => 1, 'answer' => "def sqr(n): return n * n\n"]); $this->check_output_contains('print(sqr(-11))'); $this->check_output_contains('print(sqr(12))'); $this->check_output_contains('print(sqr(-7))'); @@ -226,7 +230,7 @@ public function test_precheck_all() { // Precheck with a right answer, but because IS_PRECHECK is true // all answers should have been doubled. - $this->process_submission(array('-precheck' => 1, 'answer' => "def sqr2(n): return n * n\n")); + $this->process_submission(['-precheck' => 1, 'answer' => "def sqr2(n): return n * n\n"]); $this->check_output_contains("Precheck only"); $this->check_output_contains('242'); $this->check_output_contains('288'); @@ -234,7 +238,7 @@ public function test_precheck_all() { $this->check_output_contains('50'); // Now check with a right answer. - $this->process_submission(array('-submit' => 1, 'answer' => "def sqr2(n): return n * n\n")); + $this->process_submission(['-submit' => 1, 'answer' => "def sqr2(n): return n * n\n"]); $this->check_output_contains('121'); $this->check_output_contains('144'); $this->check_output_contains('49'); @@ -243,4 +247,3 @@ public function test_precheck_all() { $this->save_quba(); } } - diff --git a/tests/prototype_test.php b/tests/prototype_test.php index 52c25e8be..998c7467a 100644 --- a/tests/prototype_test.php +++ b/tests/prototype_test.php @@ -35,6 +35,9 @@ require_once($CFG->dirroot . '/question/format/xml/format.php'); require_once($CFG->dirroot . '/question/engine/tests/helpers.php'); +/** + * @coversNothing + */ class prototype_test extends \qtype_coderunner_testcase { protected function setUp(): void { parent::setUp(); @@ -54,9 +57,9 @@ public function test_inheritance_from_prototype() { public function test_files_inherited() { $q = $this->make_parent_and_child(); $code = "print(open('data.txt').read())"; - $response = array('answer' => $code); + $response = ['answer' => $code]; $result = $q->grade_response($response); - list($mark, $grade, $cache) = $result; + [, , $cache] = $result; $testoutcome = unserialize($cache['_testoutcome']); $this->assertTrue($testoutcome->all_correct()); } @@ -72,8 +75,8 @@ public function test_params_inherited() { EOTEMPLATE; $q->allornothing = false; $q->iscombinatortemplate = false; - $q->testcases = array( - (object) array('type' => 0, + $q->testcases = [ + (object) ['type' => 0, 'testcode' => '', 'expected' => "1 200 2", 'stdin' => '', @@ -81,15 +84,15 @@ public function test_params_inherited() { 'useasexample' => 0, 'display' => 'SHOW', 'mark' => 1.0, - 'hiderestiffail' => 0), - ); + 'hiderestiffail' => 0], + ]; $q->allornothing = false; $q->iscombinatortemplate = false; $code = ""; - $response = array('answer' => $code); + $response = ['answer' => $code]; $q->start_attempt(null); $result = $q->grade_response($response); - list($mark, $grade, $cache) = $result; + [$mark, $grade, $cache] = $result; $testoutcome = unserialize($cache['_testoutcome']); $this->assertTrue($testoutcome->all_correct()); } @@ -134,7 +137,7 @@ public function test_export() { - + 1 @@ -145,6 +148,7 @@ public function test_export() { 0 + 1 twig 0 @@ -192,17 +196,23 @@ private function make_parent_and_child() { $fs = get_file_storage(); // Prepare file record object. - $fileinfo = array( + $fileinfo = [ 'contextid' => 1, // ID of context for prototype. 'component' => 'qtype_coderunner', 'filearea' => 'datafile', 'itemid' => $id, 'filepath' => '/', - 'filename' => 'data.txt'); + 'filename' => 'data.txt']; // Create file (deleting any existing version first). - $file = $fs->get_file($fileinfo['contextid'], $fileinfo['component'], $fileinfo['filearea'], - $fileinfo['itemid'], $fileinfo['filepath'], $fileinfo['filename']); + $file = $fs->get_file( + $fileinfo['contextid'], + $fileinfo['component'], + $fileinfo['filearea'], + $fileinfo['itemid'], + $fileinfo['filepath'], + $fileinfo['filename'] + ); $file->delete(); $fs->create_file_from_string($fileinfo, "This is data\nLine 2"); @@ -211,8 +221,10 @@ private function make_parent_and_child() { } public function assert_same_xml($expectedxml, $xml) { - $this->assertEquals(str_replace("\r\n", "\n", $expectedxml), - str_replace("\r\n", "\n", $xml)); + $this->assertEquals( + str_replace("\r\n", "\n", $expectedxml), + str_replace("\r\n", "\n", $xml) + ); } // Support function to make and save a prototype question. @@ -225,7 +237,7 @@ private function make_sqr_user_type_prototype($fileattachmentreqd = false) { global $DB; $q = $this->make_question('sqr'); $q->name = 'PROTOTYPE_sqr_user_prototype'; - $q->testcases = array(); // No testcases in a prototype. + $q->testcases = []; // No testcases in a prototype. $q->prototypetype = 2; $q->coderunnertype = "sqr_user_prototype"; $q->cputimelimitsecs = 29; // Arbitrary test value. @@ -239,8 +251,9 @@ private function make_sqr_user_type_prototype($fileattachmentreqd = false) { $row = new \qtype_coderunner_question(); \test_question_maker::initialise_a_question($row); $catrow = $DB->get_record_select( // Find the question category for system context (1). - 'question_categories', - "contextid=1 limit 1"); + 'question_categories', + "contextid=1 limit 1" + ); $q->category = $catrow->id; $row->qtype = 'coderunner'; @@ -250,13 +263,13 @@ private function make_sqr_user_type_prototype($fileattachmentreqd = false) { if ($fileattachmentreqd) { // Attach a file. $fs = get_file_storage(); - $fileinfo = array( + $fileinfo = [ 'contextid' => 1, 'component' => 'qtype_coderunner', 'filearea' => 'datafile', 'itemid' => $q->id, 'filepath' => '/', - 'filename' => 'data.txt'); + 'filename' => 'data.txt']; // Create file. $fs->create_file_from_string($fileinfo, "This is data\nLine 2"); diff --git a/tests/pythonpylint_test.php b/tests/pythonpylint_test.php index 46a1219a0..09c89e744 100644 --- a/tests/pythonpylint_test.php +++ b/tests/pythonpylint_test.php @@ -33,8 +33,15 @@ /** * Unit tests for the coderunner question definition class. + * @coversNothing */ class pythonpylint_test extends \qtype_coderunner_testcase { + protected function setUp(): void { + parent::setUp(); + + // Each test will be skipped if python3 not available on jobe server. + $this->check_language_available('python3'); + } public function test_pylint_func_good() { // Test that a python3_pylint question with a good pylint-compatible. @@ -51,9 +58,9 @@ public function test_pylint_func_good() { return 0 EOCODE; - $response = array('answer' => $code); + $response = ['answer' => $code]; $result = $q->grade_response($response); - list($mark, $grade, $cache) = $result; + [, $grade, ] = $result; $this->assertEquals(\question_state::$gradedright, $grade); } @@ -67,9 +74,9 @@ public function test_pylint_func_bad() { return n * n EOCODE; - $response = array('answer' => $code); + $response = ['answer' => $code]; $result = $q->grade_response($response); - list($mark, $grade, $cache) = $result; + [$mark, $grade, $cache] = $result; $this->assertEquals(\question_state::$gradedwrong, $grade); } } diff --git a/tests/pythonquestions_test.php b/tests/pythonquestions_test.php index 921115b76..ed633bded 100644 --- a/tests/pythonquestions_test.php +++ b/tests/pythonquestions_test.php @@ -35,23 +35,32 @@ /** * Unit tests for the coderunner question definition class. + * @coversNothing */ class pythonquestions_test extends \qtype_coderunner_testcase { + + /** @var string */ + private $goodcode; + protected function setUp(): void { parent::setUp(); + + // Each test will be skipped if python3 not available on jobe server. + $this->check_language_available('python3'); + $this->goodcode = "def sqr(n): return n * n"; } public function test_summarise_response() { $s = $this->goodcode; $q = $this->make_question('sqr'); - $this->assertEquals($s, $q->summarise_response(array('answer' => $s))); + $this->assertEquals($s, $q->summarise_response(['answer' => $s])); } public function test_grade_response_right() { $q = $this->make_question('sqr'); - $response = array('answer' => $this->goodcode); - list($mark, $grade, $cache) = $q->grade_response($response); + $response = ['answer' => $this->goodcode]; + [$mark, $grade, $cache] = $q->grade_response($response); $this->assertEquals(1, $mark); $this->assertEquals(\question_state::$gradedright, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -65,8 +74,8 @@ public function test_grade_response_right() { public function test_grade_response_wrong_ans() { $q = $this->make_question('sqr'); $code = "def sqr(x): return x * x * x / abs(x)"; - $response = array('answer' => $code); - list($mark, $grade, $cache) = $q->grade_response($response); + $response = ['answer' => $code]; + [$mark, $grade, $cache] = $q->grade_response($response); $this->assertEquals(0, $mark); $this->assertEquals(\question_state::$gradedwrong, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -75,8 +84,8 @@ public function test_grade_response_wrong_ans() { public function test_grade_syntax_error() { $q = $this->make_question('sqr'); $code = "def sqr(x): return x x"; - $response = array('answer' => $code); - list($mark, $grade, $cache) = $q->grade_response($response); + $response = ['answer' => $code]; + [$mark, $grade, $cache] = $q->grade_response($response); $this->assertEquals(0, $mark); $this->assertEquals(\question_state::$gradedwrong, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -88,9 +97,9 @@ public function test_grade_syntax_error() { public function test_grade_runtime_error() { $q = $this->make_question('sqr'); $code = "def sqr(x): return x * y"; - $response = array('answer' => $code); + $response = ['answer' => $code]; $result = $q->grade_response($response); - list($mark, $grade, $cache) = $result; + [$mark, $grade, $cache] = $result; $this->assertEquals(0, $mark); $this->assertEquals(\question_state::$gradedwrong, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -102,9 +111,9 @@ public function test_grade_runtime_error() { public function test_student_answer_variable() { $q = $this->make_question('studentanswervar'); $code = "\"\"\"Line1\n\"Line2\"\n'Line3'\nLine4\n\"\"\""; - $response = array('answer' => $code); + $response = ['answer' => $code]; $result = $q->grade_response($response); - list($mark, $grade, $cache) = $result; + [$mark, $grade, ] = $result; $this->assertEquals(1, $mark); $this->assertEquals(\question_state::$gradedright, $grade); } @@ -112,9 +121,9 @@ public function test_student_answer_variable() { public function test_illegal_open_error() { $q = $this->make_question('sqr'); $code = "def sqr(x):\n f = open('/twaddle/blah/xxx');\n return x * x"; - $response = array('answer' => $code); + $response = ['answer' => $code]; $result = $q->grade_response($response); - list($mark, $grade, $cache) = $result; + [$mark, $grade, $cache] = $result; $this->assertEquals(0, $mark); $this->assertEquals(\question_state::$gradedwrong, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -126,9 +135,9 @@ public function test_illegal_open_error() { public function test_grade_delayed_runtime_error() { $q = $this->make_question('sqr'); $code = "def sqr(x):\n if x != 11:\n return x * x\n else:\n return y"; - $response = array('answer' => $code); + $response = ['answer' => $code]; $result = $q->grade_response($response); - list($mark, $grade, $cache) = $result; + [$mark, $grade, $cache] = $result; $this->assertEquals(0, $mark); $this->assertEquals(\question_state::$gradedwrong, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -146,9 +155,9 @@ public function test_triple_quotes() { that squares its parameter""" return x * x EOCODE; - $response = array('answer' => $code); + $response = ['answer' => $code]; $result = $q->grade_response($response); - list($mark, $grade, $cache) = $result; + [$mark, $grade, $cache] = $result; $this->assertEquals(1, $mark); $this->assertEquals(\question_state::$gradedright, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -163,9 +172,9 @@ public function test_hellofunc() { // Check a question type with a function that prints output. $q = $this->make_question('hello_func'); $code = "def sayHello(name):\n print('Hello ' + name)"; - $response = array('answer' => $code); + $response = ['answer' => $code]; $result = $q->grade_response($response); - list($mark, $grade, $cache) = $result; + [$mark, $grade, $cache] = $result; $this->assertEquals(1, $mark); $this->assertEquals(\question_state::$gradedright, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -185,9 +194,9 @@ public function test_copystdin() { line = input() print(line) EOCODE; - $response = array('answer' => $code); + $response = ['answer' => $code]; $result = $q->grade_response($response); - list($mark, $grade, $cache) = $result; + [$mark, $grade, $cache] = $result; $this->assertEquals(0, $mark); $this->assertEquals(\question_state::$gradedwrong, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -205,9 +214,9 @@ public function test_timeout() { // Check a question that loops forever. Should cause sandbox timeout. $q = $this->make_question('timeout'); $code = "def timeout():\n while (1):\n pass"; - $response = array('answer' => $code); + $response = ['answer' => $code]; $result = $q->grade_response($response); - list($mark, $grade, $cache) = $result; + [$mark, $grade, $cache] = $result; $this->assertEquals(0, $mark); $this->assertEquals(\question_state::$gradedwrong, $grade); $this->assertTrue(isset($cache['_testoutcome'])); @@ -221,47 +230,49 @@ public function test_exceptions() { // Check a function that conditionally throws exceptions. $q = $this->make_question('exceptions'); $code = "def checkOdd(n):\n if n & 1:\n raise ValueError()"; - $response = array('answer' => $code); + $response = ['answer' => $code]; $result = $q->grade_response($response); - list($mark, $grade, $cache) = $result; + [$mark, $grade, $cache] = $result; $this->assertEquals(1, $mark); $this->assertEquals(\question_state::$gradedright, $grade); $this->assertTrue(isset($cache['_testoutcome'])); $testoutcome = unserialize($cache['_testoutcome']); $this->assertEquals(2, count($testoutcome->testresults)); $this->assertEquals("Exception\n", $testoutcome->testresults[0]->got); - $this->assertEquals("Yes\nYes\nNo\nNo\nYes\nNo\n", - $testoutcome->testresults[1]->got); + $this->assertEquals( + "Yes\nYes\nNo\nNo\nYes\nNo\n", + $testoutcome->testresults[1]->got + ); } public function test_partial_mark_question() { // Test a question that isn't of the usual allornothing variety. $q = $this->make_question('sqr_part_marks'); $code = "def sqr(n):\n return -17.995"; - $response = array('answer' => $code); + $response = ['answer' => $code]; $result = $q->grade_response($response); - list($mark, $grade, $cache) = $result; + [$mark, $grade, $cache] = $result; $this->assertEquals(\question_state::$gradedpartial, $grade); $this->assertEquals(0, $mark); $code = "def sqr(n):\n return 0"; // Passes first test only. - $response = array('answer' => $code); + $response = ['answer' => $code]; $result = $q->grade_response($response); - list($mark, $grade, $cache) = $result; + [$mark, $grade, $cache] = $result; $this->assertEquals(\question_state::$gradedpartial, $grade); $this->assertTrue(abs($mark - 0.5 / 7.5) < 0.00001); $code = "def sqr(n):\n return n * n if n <= 0 else -17.995"; // Passes first test and last two only. - $response = array('answer' => $code); + $response = ['answer' => $code]; $result = $q->grade_response($response); - list($mark, $grade, $cache) = $result; + [$mark, $grade, $cache] = $result; $this->assertEquals(\question_state::$gradedpartial, $grade); $this->assertTrue(abs($mark - 5.0 / 7.5) < 0.00001); $code = "def sqr(n):\n return n * n if n <= 0 else 1 / 0"; // Passes first test then aborts. - $response = array('answer' => $code); + $response = ['answer' => $code]; $result = $q->grade_response($response); - list($mark, $grade, $cache) = $result; + [$mark, $grade, $cache] = $result; $this->assertEquals(\question_state::$gradedpartial, $grade); $this->assertTrue(abs($mark - 0.5 / 7.5) < 0.00001); } @@ -273,12 +284,12 @@ public function test_customised_timeout() { sleep(10) # Wait 10 seconds print("Hello Python") EOT; - $response = array('answer' => $slowsquare); // Should time out. - list($mark, $grade, $cache) = $q->grade_response($response); + $response = ['answer' => $slowsquare]; // Should time out. + [$mark, $grade, $cache] = $q->grade_response($response); $this->assertEquals(0, $mark); $this->assertEquals(\question_state::$gradedwrong, $grade); $q->cputimelimitsecs = 20; // This should fix it. - list($mark, $grade, $cache) = $q->grade_response($response); + [$mark, $grade, $cache] = $q->grade_response($response); $this->assertEquals(1, $mark); $this->assertEquals(\question_state::$gradedright, $grade); } diff --git a/tests/questiontype_test.php b/tests/questiontype_test.php index 99bb819e8..538d12c74 100644 --- a/tests/questiontype_test.php +++ b/tests/questiontype_test.php @@ -38,7 +38,7 @@ /** * Unit tests for the coderunner question type class. - * + * @coversNothing * @copyright 2021 Richard Lobb, The University of Canterbury * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -73,7 +73,6 @@ public function test_get_random_guess_score() { public function test_get_possible_responses() { $q = $this->get_test_question_data(); - $this->assertEquals(array(), $this->qtype->get_possible_responses($q)); + $this->assertEquals([], $this->qtype->get_possible_responses($q)); } - } diff --git a/tests/restore_test.php b/tests/restore_test.php index aa73dcdef..c3088f78e 100644 --- a/tests/restore_test.php +++ b/tests/restore_test.php @@ -36,6 +36,7 @@ /** * Unit tests for CodeRunner restore code. * + * @coversNothing * @copyright 2016 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -72,10 +73,18 @@ protected function restore_backup($backupfile) { // Restore one of the example backups. $newcourseid = \restore_dbops::create_new_course( - 'Restore test', 'RT100', $this->category->id); - $rc = new \restore_controller($folder, $newcourseid, - \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id, - \backup::TARGET_NEW_COURSE); + 'Restore test', + 'RT100', + $this->category->id + ); + $rc = new \restore_controller( + $folder, + $newcourseid, + \backup::INTERACTIVE_NO, + \backup::MODE_GENERAL, + $USER->id, + \backup::TARGET_NEW_COURSE + ); $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); @@ -92,12 +101,18 @@ protected function restore_backup($backupfile) { */ protected function load_question_data_by_name($name) { global $DB; - $questionid = $DB->get_field('question', 'id', array('name' => $name), MUST_EXIST); - return array( - $DB->get_record('question_coderunner_options', - array('questionid' => $questionid), '*', MUST_EXIST), - $DB->get_records('question_coderunner_tests', - array('questionid' => $questionid))); + $questionid = $DB->get_field('question', 'id', ['name' => $name], MUST_EXIST); + return [ + $DB->get_record( + 'question_coderunner_options', + ['questionid' => $questionid], + '*', + MUST_EXIST + ), + $DB->get_records( + 'question_coderunner_tests', + ['questionid' => $questionid] + )]; } public function test_restore() { @@ -107,11 +122,11 @@ public function test_restore() { '/question/type/coderunner/tests/fixtures/loadtesting_pseudocourse_backup.mbz'); // Verify some restored questions look OK. - list($options, $tests) = $question = $this->load_question_data_by_name('c_to_fpy3'); + [$options, $tests] = $this->load_question_data_by_name('c_to_fpy3'); $this->assertCount(3, $tests); $this->assertNull($options->template); - list($options, $tests) = $this->load_question_data_by_name('PROTOTYPE_clojure_with_combinator'); + [$options, $tests] = $this->load_question_data_by_name('PROTOTYPE_clojure_with_combinator'); $this->assertCount(1, $tests); $this->assertStringStartsWith('import subprocess', $options->template); } @@ -123,11 +138,11 @@ public function test_restore_from_v3_0_0() { '/question/type/coderunner/tests/fixtures/loadtesting_pseudocourse_backup_V3.0.0.mbz'); // Verify some restored questions look OK. - list($options, $tests) = $question = $this->load_question_data_by_name('c_to_fpy3'); + [$options, $tests] = $this->load_question_data_by_name('c_to_fpy3'); $this->assertCount(3, $tests); $this->assertNull($options->template); - list($options, $tests) = $this->load_question_data_by_name('PROTOTYPE_clojure_with_combinator'); + [$options, $tests] = $this->load_question_data_by_name('PROTOTYPE_clojure_with_combinator'); $this->assertCount(1, $tests); $this->assertStringStartsWith('import subprocess', $options->template); } diff --git a/tests/template_test.php b/tests/template_test.php index e9a80295a..d821311bb 100644 --- a/tests/template_test.php +++ b/tests/template_test.php @@ -34,24 +34,24 @@ require_once($CFG->dirroot . '/question/type/coderunner/classes/twigmacros.php'); /** - * Unit tests for the coderunner question definition class. + * Unit tests for the behaviour of coderunner question templates. + * @coversNothing */ class template_test extends \qtype_coderunner_testcase { - public function test_template_engine() { // Check if the template engine is installed and working OK. $macros = \qtype_coderunner_twigmacros::macros(); $twigloader = new \Twig\Loader\ArrayLoader($macros); - $twigoptions = array( + $twigoptions = [ 'cache' => false, 'optimizations' => 0, 'autoescape' => false, 'strict_variables' => true, - 'debug' => true); + 'debug' => true]; $twig = new \Twig\Environment($twigloader, $twigoptions); $template = $twig->createTemplate('Hello {{ name }}!'); - $renderedstring = $template->render(array('name' => 'Fabien')); + $renderedstring = $template->render(['name' => 'Fabien']); $this->assertEquals('Hello Fabien!', $renderedstring); } @@ -68,8 +68,8 @@ public function test_question_template() { EOTEMPLATE; $q->iscombinatortemplate = false; $q->allornothing = false; - $q->testcases = array( - (object) array('testtype' => 0, + $q->testcases = [ + (object) ['testtype' => 0, 'testcode' => 'print(sqr(-3))', 'expected' => "9\ntwiddle-twaddle", 'stdin' => '', @@ -77,12 +77,12 @@ public function test_question_template() { 'useasexample' => 0, 'display' => 'SHOW', 'mark' => 1.0, - 'hiderestiffail' => 0), - ); + 'hiderestiffail' => 0], + ]; $code = "def sqr(n): return n * n\n"; - $response = array('answer' => $code); + $response = ['answer' => $code]; $result = $q->grade_response($response); - list($mark, $grade, $cache) = $result; + [$mark, $grade, $cache] = $result; $this->assertEquals(\question_state::$gradedright, $grade); } @@ -110,13 +110,13 @@ public function test_grading_template() { $q->iscombinatortemplate = false; $q->allornothing = false; $code = "def sqr(n): return n * n\n"; - $response = array('answer' => $code); + $response = ['answer' => $code]; $result = $q->grade_response($response); - list($mark, $grade, $cache) = $result; + [$mark, $grade, $cache] = $result; $this->assertTrue(abs($mark - 24.0 / 31.0) < 0.000001); $q->allornothing = true; $result = $q->grade_response($response); - list($mark, $grade, $cache) = $result; + [$mark, $grade, $cache] = $result; $this->assertTrue($mark == 0.0); } @@ -134,8 +134,8 @@ public function test_template_params() { EOTEMPLATE; $q->allornothing = false; $q->iscombinatortemplate = false; - $q->testcases = array( - (object) array('type' => 0, + $q->testcases = [ + (object) ['type' => 0, 'testcode' => '', 'expected' => "23 blah", 'stdin' => '', @@ -143,14 +143,14 @@ public function test_template_params() { 'useasexample' => 0, 'display' => 'SHOW', 'mark' => 1.0, - 'hiderestiffail' => 0), - ); + 'hiderestiffail' => 0], + ]; $q->allornothing = false; $q->iscombinatortemplate = false; $code = ""; - $response = array('answer' => $code); + $response = ['answer' => $code]; $result = $q->grade_response($response); - list($mark, $grade, $cache) = $result; + [$mark, $grade, $cache] = $result; $this->assertEquals(\question_state::$gradedright, $grade); } } diff --git a/tests/test.php b/tests/test.php index 81c752958..bfd582022 100644 --- a/tests/test.php +++ b/tests/test.php @@ -30,17 +30,23 @@ require_once($CFG->dirroot . '/question/engine/tests/helpers.php'); require_once($CFG->dirroot . '/question/type/coderunner/question.php'); +/** + * @coversNothing + */ class qtype_coderunner_testcase extends advanced_testcase { - protected $hasfailed = false; // Set to true when a test fails. + /** @var stdClass Holds question category.*/ + protected $category; + protected function setUp(): void { parent::setUp(); self::setup_test_sandbox_configuration(); $this->resetAfterTest(false); $this->setAdminUser(); $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); - $this->category = $generator->create_question_category(array()); + $this->category = $generator->create_question_category([]); + $this->check_sandbox_enabled('jobesandbox'); } /** @@ -66,7 +72,7 @@ public static function setup_test_sandbox_configuration(): void { // to conditionally skip later tests. See jobesendbox_test. // Name can't be made moodle-standards compliant as it's defined by phpunit. // $e is the exception to be thrown. - protected function onNotSuccessfulTest(Throwable $e): void { + protected function onnotsuccessfultest(Throwable $e): void { $this->hasfailed = true; throw $e; } @@ -80,7 +86,8 @@ public function test_dummy(): void { protected function check_language_available($language): void { if (qtype_coderunner_sandbox::get_best_sandbox($language, true) === null) { $this->markTestSkipped( - "$language is not installed on your server. Test skipped."); + "$language is not installed on your server. Test skipped." + ); } } diff --git a/tests/ui_parameters_test.php b/tests/ui_parameters_test.php index 1f82cc96b..faa66db03 100644 --- a/tests/ui_parameters_test.php +++ b/tests/ui_parameters_test.php @@ -34,9 +34,9 @@ /** * Unit tests for UI parameters + * @coversNothing */ class ui_parameters_test extends \qtype_coderunner_testcase { - // Test that the json specifier for the graph_ui class can be loaded. public function test_params() { $graphuiparams = new \qtype_coderunner_ui_parameters('graph'); @@ -51,7 +51,11 @@ public function test_params() { $this->assertContains('noderadius', array_keys($paramsarray)); $this->assertEquals(30, $paramsarray['noderadius']); $aceparams = new \qtype_coderunner_ui_parameters('ace'); - $this->assertEmpty($aceparams->all_names()); + $this->assertEquals(false, $aceparams->value('auto_switch_light_dark')); + $this->assertEquals("14px", $aceparams->value('font_size')); + $this->assertEquals(true, $aceparams->value('import_from_scratchpad')); + $this->assertEquals(false, $aceparams->value('live_autocompletion')); + $this->assertEquals("textmate", $aceparams->value('theme')); } // Test that we can get a list of all plugins and their parameter lists. @@ -60,11 +64,9 @@ public function test_plugin_list() { $names = $plugins->all_names(); $this->assertContains('ace', $names); $this->assertContains('graph', $names); - $aceparams = $plugins->parameters('ace'); - $this->assertEquals(0, $aceparams->length()); - $graphparams = $plugins->parameters('graph'); - $this->assertEquals(26, $graphparams->value('noderadius')); - $this->assertContains('ace', $plugins->all_with_no_params()); + $this->assertContains('gapfiller', $names); + $this->assertContains('html', $names); + $this->assertContains('scratchpad', $names); } // Test the dropdown list for the plugins. diff --git a/tests/walkthrough_combinator_grader_test.php b/tests/walkthrough_combinator_grader_test.php index a98109ff2..b3cd967b5 100644 --- a/tests/walkthrough_combinator_grader_test.php +++ b/tests/walkthrough_combinator_grader_test.php @@ -36,17 +36,18 @@ /** * Unit tests for the coderunner question type. * + * @coversNothing * @copyright 2011, 2020 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - - class walkthrough_combinator_grader_test extends \qbehaviour_walkthrough_test_base { - protected function setUp(): void { global $CFG; parent::setUp(); \qtype_coderunner_testcase::setup_test_sandbox_configuration(); + if (!get_config('qtype_coderunner', 'jobesandbox_enabled')) { + $this->markTestSkipped("Jobe sandbox unavailable: test skipped"); + } } public function test_combinator_template_grading() { @@ -78,17 +79,17 @@ public function test_combinator_template_grading() { // Submit a right answer. $this->start_attempt_at_question($q, 'adaptive', 1, 1); - $this->process_submission(array('-submit' => 1, - 'answer' => "hi di hi and HO DI HO")); + $this->process_submission(['-submit' => 1, + 'answer' => "hi di hi and HO DI HO"]); $this->check_current_mark(1.0); - $this->check_current_output( new \question_pattern_expectation('|

Well done

|') ); + $this->check_current_output(new \question_pattern_expectation('|

Well done

|')); // Submit a partially right answer. $this->start_attempt_at_question($q, 'adaptive', 1, 1); - $this->process_submission(array('-submit' => 1, - 'answer' => "hi di nothi and HO DI NOTHO")); + $this->process_submission(['-submit' => 1, + 'answer' => "hi di nothi and HO DI NOTHO"]); $this->check_current_mark(0.5); - $this->check_current_output( new \question_pattern_expectation('|

Wrong numbers of hi and/or ho

|') ); + $this->check_current_output(new \question_pattern_expectation('|

Wrong numbers of hi and/or ho

|')); } @@ -120,8 +121,8 @@ public function test_combinator_template_grading2() { // Submit a right answer. $this->start_attempt_at_question($q, 'adaptive', 1, 1); - $this->process_submission(array('-submit' => 1, - 'answer' => "def sqr(n): return n * n")); + $this->process_submission(['-submit' => 1, + 'answer' => "def sqr(n): return n * n"]); $this->check_current_mark(1.0); $this->check_output_contains('Prologue'); $this->check_output_contains('Expected'); @@ -132,8 +133,8 @@ public function test_combinator_template_grading2() { // Submit a partially right answer. $this->start_attempt_at_question($q, 'adaptive', 1, 1); - $this->process_submission(array('-submit' => 1, - 'answer' => "def sqr(n): return n * n if n != -5 else 'Bin' + 'Go!'")); + $this->process_submission(['-submit' => 1, + 'answer' => "def sqr(n): return n * n if n != -5 else 'Bin' + 'Go!'"]); $this->check_output_contains('Prologue'); $this->check_output_contains('Expected'); $this->check_output_contains('Got'); @@ -159,14 +160,14 @@ public function test_bad_combinator_error() { $q->grader = 'EqualityGrader'; // Submit a right answer. $this->start_attempt_at_question($q, 'adaptive', 1, 1); - $this->process_submission(array('-submit' => 1, - 'answer' => 'def sqr(n): return n * n')); + $this->process_submission(['-submit' => 1, + 'answer' => 'def sqr(n): return n * n']); $this->check_current_mark(0.0); $this->check_output_contains('Perhaps excessive output or error in question?'); } // Test that if the combinator grader outputs bad JSON, we get an - // appropriate error message + // appropriate error message. public function test_bad_json() { $q = \test_question_maker::make_question('coderunner', 'sqr'); $q->template = <<grader = 'TemplateGrader'; // Submit a right answer. $this->start_attempt_at_question($q, 'adaptive', 1, 1); - $this->process_submission(array('-submit' => 1, - 'answer' => 'def sqr(n): return n * n')); + $this->process_submission(['-submit' => 1, + 'answer' => 'def sqr(n): return n * n']); $this->check_current_mark(0.0); - $this->check_output_contains('Bad JSON output from combinator grader output. Output was: twaddle'); + $this->check_output_contains('Bad JSON output from combinator grader. Output was: twaddle'); } // Test that if the combinator grader output has a missing fraction attribute @@ -201,8 +202,8 @@ public function test_missing_fraction() { $q->grader = 'TemplateGrader'; // Submit a right answer. $this->start_attempt_at_question($q, 'adaptive', 1, 1); - $this->process_submission(array('-submit' => 1, - 'answer' => 'def sqr(n): return n * n')); + $this->process_submission(['-submit' => 1, + 'answer' => 'def sqr(n): return n * n']); $this->check_current_mark(0.0); $this->check_output_contains('Bad or missing fraction in output from template grader'); } @@ -224,8 +225,8 @@ public function test_bad_fraction() { $q->grader = 'TemplateGrader'; // Submit a right answer. $this->start_attempt_at_question($q, 'adaptive', 1, 1); - $this->process_submission(array('-submit' => 1, - 'answer' => 'def sqr(n): return n * n')); + $this->process_submission(['-submit' => 1, + 'answer' => 'def sqr(n): return n * n']); $this->check_current_mark(0.0); $this->check_output_contains('Bad or missing fraction in output from template grader'); } @@ -248,8 +249,8 @@ public function test_show_output_only() { $q->grader = 'TemplateGrader'; // Submit a right answer. $this->start_attempt_at_question($q, 'adaptive', 1, 1); - $this->process_submission(array('-submit' => 1, - 'answer' => 'def sqr(n): return n * n')); + $this->process_submission(['-submit' => 1, + 'answer' => 'def sqr(n): return n * n']); $this->check_current_mark(1.0); $this->check_output_contains("Prologue"); $this->check_output_contains("Wasn't that FUN!"); @@ -283,8 +284,8 @@ public function test_bad_combinator_grader_error() { $q->grader = 'TemplateGrader'; // Submit a right answer. $this->start_attempt_at_question($q, 'adaptive', 1, 1); - $this->process_submission(array('-submit' => 1, - 'answer' => 'def sqr(n): return n * n')); + $this->process_submission(['-submit' => 1, + 'answer' => 'def sqr(n): return n * n']); $this->check_current_mark(0.0); $this->check_output_contains('Wrong number of test results column formats. Expected 6, got 7'); } @@ -317,8 +318,8 @@ public function test_bad_combinator_grader_error2() { $q->grader = 'TemplateGrader'; // Submit a right answer. $this->start_attempt_at_question($q, 'adaptive', 1, 1); - $this->process_submission(array('-submit' => 1, - 'answer' => 'def sqr(n): return n * n')); + $this->process_submission(['-submit' => 1, + 'answer' => 'def sqr(n): return n * n']); $this->check_current_mark(0.0); $this->check_output_contains('Unknown field name (columnformatt) in combinator grader output'); } @@ -351,8 +352,8 @@ public function test_bad_combinator_grader_error3() { $q->grader = 'TemplateGrader'; // Submit a right answer. $this->start_attempt_at_question($q, 'adaptive', 1, 1); - $this->process_submission(array('-submit' => 1, - 'answer' => 'def sqr(n): return n * n')); + $this->process_submission(['-submit' => 1, + 'answer' => 'def sqr(n): return n * n']); $this->check_current_mark(0.0); $this->check_output_contains('Illegal format (%x) in columnformats'); } @@ -380,13 +381,15 @@ public function test_graderstate_in_stepinfo() { $q->grader = 'TemplateGrader'; // Submit a right answer. $this->start_attempt_at_question($q, 'adaptive', 1, 1); - $this->process_submission(array('-submit' => 1, - 'answer' => 'def sqr(n): return n * n')); + $this->process_submission(['-submit' => 1, + 'answer' => 'def sqr(n): return n * n']); $this->check_output_contains("Prologue"); $this->check_output_contains("graderstate: Empty"); $this->check_output_does_not_contain('Passed all tests'); - $this->process_submission(array('-submit' => 1, - 'answer' => 'def sqr(n): return n * n # resubmit')); + $this->process_submission(['-submit' => 1, + 'answer' => 'def sqr(n): return n * n # resubmit']); $this->check_output_contains("graderstate: boomerang"); } + + } diff --git a/tests/walkthrough_display_feedback_test.php b/tests/walkthrough_display_feedback_test.php index 5df46ae80..98cce2dda 100644 --- a/tests/walkthrough_display_feedback_test.php +++ b/tests/walkthrough_display_feedback_test.php @@ -14,17 +14,6 @@ // You should have received a copy of the GNU General Public License // along with CodeRunner. If not, see . -/** - * Use a walkthrough test to validate the new (2019) display-feedback options. - * @group qtype_coderunner - * - * @package qtype - * @subpackage coderunner - * @copyright 2019 Richard Lobb, The University of Canterbury - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - - namespace qtype_coderunner; defined('MOODLE_INTERNAL') || die(); @@ -34,12 +23,23 @@ require_once($CFG->dirroot . '/question/type/coderunner/tests/test.php'); require_once($CFG->dirroot . '/question/type/coderunner/question.php'); -class walkthrough_display_feedback_test extends \qbehaviour_walkthrough_test_base { +/** + * Use a walkthrough test to validate the new (2019) display-feedback options. + * @group qtype_coderunner + * @coversNothing + * @package qtype + * @subpackage coderunner + * @copyright 2019 Richard Lobb, The University of Canterbury + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class walkthrough_display_feedback_test extends \qbehaviour_walkthrough_test_base { protected function setUp(): void { - global $CFG; parent::setUp(); \qtype_coderunner_testcase::setup_test_sandbox_configuration(); + if (!get_config('qtype_coderunner', 'jobesandbox_enabled')) { + $this->markTestSkipped("Jobe sandbox unavailable: test skipped"); + } } @@ -51,18 +51,18 @@ public function test_display_feedback_adaptive() { $q = \test_question_maker::make_question('coderunner', 'sqr'); $this->assertEquals($q->displayfeedback, 1); $this->start_attempt_at_question($q, 'adaptive', 1); - $this->process_submission(array('-submit' => 1, - 'answer' => 'def sqr(n): return n * n')); + $this->process_submission(['-submit' => 1, + 'answer' => 'def sqr(n): return n * n']); $this->check_output_contains('Passed all tests'); $q->displayfeedback = 0; // Should be the same outcome (quiz default). $this->start_attempt_at_question($q, 'adaptive', 1); - $this->process_submission(array('-submit' => 1, - 'answer' => 'def sqr(n): return n * n')); + $this->process_submission(['-submit' => 1, + 'answer' => 'def sqr(n): return n * n']); $this->check_output_contains('Passed all tests'); $q->displayfeedback = 2; // But with a setting of 2, should be no result table. $this->start_attempt_at_question($q, 'adaptive', 1); - $this->process_submission(array('-submit' => 1, - 'answer' => 'def sqr(n): return n * n')); + $this->process_submission(['-submit' => 1, + 'answer' => 'def sqr(n): return n * n']); $this->check_output_does_not_contain('Passed all tests'); } @@ -78,18 +78,18 @@ public function test_display_feedback_deferred() { $q->displayfeedback = 0; // Uses quiz feedback setting. $this->start_attempt_at_question($q, 'deferredfeedback', 1); $this->displayoptions->feedback = 0; // Seems we have to set this explicitly. - $this->process_submission(array('-submit' => 1, - 'answer' => 'def sqr(n): return n * n')); + $this->process_submission(['-submit' => 1, + 'answer' => 'def sqr(n): return n * n']); $this->check_output_does_not_contain('Passed all tests'); $q->displayfeedback = 1; $this->start_attempt_at_question($q, 'deferredfeedback', 1); - $this->process_submission(array('-submit' => 1, - 'answer' => 'def sqr(n): return n * n')); + $this->process_submission(['-submit' => 1, + 'answer' => 'def sqr(n): return n * n']); $this->check_output_contains('Passed all tests'); $q->displayfeedback = 2; $this->start_attempt_at_question($q, 'deferredfeedback', 1); - $this->process_submission(array('-submit' => 1, - 'answer' => 'def sqr(n): return n * n')); + $this->process_submission(['-submit' => 1, + 'answer' => 'def sqr(n): return n * n']); $this->check_output_does_not_contain('Passed all tests'); } } diff --git a/tests/walkthrough_extras_test.php b/tests/walkthrough_extras_test.php index f06470e5d..f5fae836a 100644 --- a/tests/walkthrough_extras_test.php +++ b/tests/walkthrough_extras_test.php @@ -14,17 +14,6 @@ // You should have received a copy of the GNU General Public License // along with CodeRunner. If not, see . -/** - * Further walkthrough tests for the CodeRunner plugin, testing recently - * added features like the 'extra' field for use by the template and the - * relabelling of output columns. - * @group qtype_coderunner - * - * @package qtype - * @subpackage coderunner - * @copyright 2012, 2014 Richard Lobb, The University of Canterbury - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ namespace qtype_coderunner; @@ -38,18 +27,30 @@ define('PRELOAD_TEST', "# TEST COMMENT TO CHECK PRELOAD IS WORKING\n"); +/** + * Further walkthrough tests for the CodeRunner plugin, testing recently + * added features like the 'extra' field for use by the template and the + * relabelling of output columns. + * @group qtype_coderunner + * @coversNothing + * @package qtype + * @subpackage coderunner + * @copyright 2012, 2014 Richard Lobb, The University of Canterbury + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ class walkthrough_extras_test extends \qbehaviour_walkthrough_test_base { - protected function setUp(): void { - global $CFG; parent::setUp(); \qtype_coderunner_testcase::setup_test_sandbox_configuration(); + if (!get_config('qtype_coderunner', 'jobesandbox_enabled')) { + $this->markTestSkipped("Jobe sandbox unavailable: test skipped"); + } } public function test_extra_testcase_field() { $q = \test_question_maker::make_question('coderunner', 'sqr'); - $q->testcases = array( - (object) array('type' => 0, + $q->testcases = [ + (object) ['type' => 0, 'testcode' => 'print("Oops")', 'extra' => 'print(sqr(-11))', 'expected' => '121', @@ -57,8 +58,8 @@ public function test_extra_testcase_field() { 'useasexample' => 0, 'display' => 'SHOW', 'mark' => 1.0, - 'hiderestiffail' => 0) - ); + 'hiderestiffail' => 0], + ]; $q->template = <<start_attempt_at_question($q, 'adaptive', 1, 1); - $this->check_current_output( new \question_pattern_expectation('/' . PRELOAD_TEST . '/') ); - $this->process_submission(array('-submit' => 1, 'answer' => "def sqr(n): return n * n\n")); + $this->check_current_output(new \question_pattern_expectation('/' . PRELOAD_TEST . '/')); + $this->process_submission(['-submit' => 1, 'answer' => "def sqr(n): return n * n\n"]); $this->check_current_mark(1.0); } @@ -81,11 +82,11 @@ public function test_result_column_selection() { // Submit a right answer. $this->start_attempt_at_question($q, 'adaptive', 1, 1); - $this->process_submission(array('-submit' => 1, 'answer' => "def sqr(n): return n * n\n")); + $this->process_submission(['-submit' => 1, 'answer' => "def sqr(n): return n * n\n"]); $this->check_current_mark(1.0); - $this->check_current_output( new \question_pattern_expectation('/Blah/') ); - $this->check_current_output( new \question_pattern_expectation('/Thing/') ); - $this->check_current_output( new \question_pattern_expectation('/Gottim/') ); + $this->check_current_output(new \question_pattern_expectation('/Blah/')); + $this->check_current_output(new \question_pattern_expectation('/Thing/')); + $this->check_current_output(new \question_pattern_expectation('/Gottim/')); } /** Make sure that if the Jobe URL is wrong we get "jobesandbox is down @@ -97,12 +98,12 @@ public function test_result_column_selection() { */ public function test_misconfigured_jobe() { if (!get_config('qtype_coderunner', 'jobesandbox_enabled')) { - $this->markTestSkipped("Sandbox $sandbox unavailable: test skipped"); + $this->markTestSkipped("Jobe sandbox unavailable: test skipped"); } set_config('jobe_host', 'localhostxxx', 'qtype_coderunner'); // Broken jobe_host url. $q = \test_question_maker::make_question('coderunner', 'sqr'); $this->start_attempt_at_question($q, 'adaptive', 1, 1); - $this->process_submission(array('-submit' => 1, 'answer' => "def sqr(n): return n * n\n")); + $this->process_submission(['-submit' => 1, 'answer' => "def sqr(n): return n * n\n"]); } /** Check that a combinator template is run once per test case when stdin @@ -116,13 +117,13 @@ public function test_multiplestdins() { // Submit a right answer. $this->start_attempt_at_question($q, 'adaptive', 1, 1); - $this->process_submission(array('-submit' => 1, 'answer' => "def sqr(n): return n * n\n")); + $this->process_submission(['-submit' => 1, 'answer' => "def sqr(n): return n * n\n"]); $this->check_output_contains('Run 4'); // Now turn on allowmultiplestdins and try again. $q->allowmultiplestdins = true; $this->start_attempt_at_question($q, 'adaptive', 1, 1); - $this->process_submission(array('-submit' => 1, 'answer' => "def sqr(n): return n * n\n")); + $this->process_submission(['-submit' => 1, 'answer' => "def sqr(n): return n * n\n"]); $this->check_output_does_not_contain('Run 4'); } } diff --git a/tests/walkthrough_multilang_test.php b/tests/walkthrough_multilang_test.php index 957af0a93..d1f0673e5 100644 --- a/tests/walkthrough_multilang_test.php +++ b/tests/walkthrough_multilang_test.php @@ -14,17 +14,6 @@ // You should have received a copy of the GNU General Public License // along with CodeRunner. If not, see . -/** - * A walkthrough of a simple multilanguage question that asks for a program - * that echos stdin to stdout. Tests all languages supported by the current - * multilanguage question type: C, C++, Java, Python3 - * @group qtype_coderunner - * - * @package qtype - * @subpackage coderunner - * @copyright 2018 Richard Lobb, The University of Canterbury - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ namespace qtype_coderunner; @@ -36,17 +25,30 @@ require_once($CFG->dirroot . '/question/type/coderunner/tests/test.php'); require_once($CFG->dirroot . '/question/type/coderunner/question.php'); -class walkthrough_multilang_test extends \qbehaviour_walkthrough_test_base { +/** + * A walkthrough of a simple multilanguage question that asks for a program + * that echos stdin to stdout. Tests all languages supported by the current + * multilanguage question type: C, C++, Java, Python3 + * @group qtype_coderunner + * @coversNothing + * @package qtype + * @subpackage coderunner + * @copyright 2018 Richard Lobb, The University of Canterbury + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class walkthrough_multilang_test extends \qbehaviour_walkthrough_test_base { protected function setUp(): void { - global $CFG; parent::setUp(); \qtype_coderunner_testcase::setup_test_sandbox_configuration(); + if (!get_config('qtype_coderunner', 'jobesandbox_enabled')) { + $this->markTestSkipped("Jobe sandbox unavailable: test skipped"); + } } public function test_echostdin() { - $answers = array( + $answers = [ 'python3' => "try:\n while 1:\n print(input())\n\nexcept:\n pass\n", 'c' => "#include \nint main() { int c; while ((c = getchar()) != EOF) { putchar(c); }}", 'cpp' => "#include \nint main () { std::cout << std::cin.rdbuf();}", @@ -62,22 +64,21 @@ public function test_echostdin() { System.out.write(buffer, 0, bytesRead); } } -}" - ); +}", + ]; $q = \test_question_maker::make_question('coderunner', 'multilang_echo_stdin'); // Submit a right answer in all languages. foreach ($answers as $lang => $answer) { $this->start_attempt_at_question($q, 'adaptive', 1, 1); $this->process_submission( - array( + [ '-submit' => 1, 'answer' => $answer, - 'language' => $lang - ) + 'language' => $lang, + ] ); $this->check_current_mark(1.0); } } } - diff --git a/tests/walkthrough_randomisation_test.php b/tests/walkthrough_randomisation_test.php index 08f2456fe..23efbe3d5 100644 --- a/tests/walkthrough_randomisation_test.php +++ b/tests/walkthrough_randomisation_test.php @@ -14,18 +14,6 @@ // You should have received a copy of the GNU General Public License // along with CodeRunner. If not, see . -/** - * Further walkthrough tests for the CodeRunner plugin, testing the - * randomisation mechanisem. - * @group qtype_coderunner - * - * @package qtype - * @subpackage coderunner - * @copyright 2018 Richard Lobb, The University of Canterbury - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - - namespace qtype_coderunner; defined('MOODLE_INTERNAL') || die(); @@ -35,10 +23,18 @@ require_once($CFG->dirroot . '/question/type/coderunner/tests/test.php'); require_once($CFG->dirroot . '/question/type/coderunner/question.php'); +/** + * Further walkthrough tests for the CodeRunner plugin, testing the + * randomisation mechanisem. + * @group qtype_coderunner + * @coversNothing + * @package qtype + * @subpackage coderunner + * @copyright 2018 Richard Lobb, The University of Canterbury + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ class walkthrough_randomisation_test extends \qbehaviour_walkthrough_test_base { - protected function setUp(): void { - global $CFG; parent::setUp(); \qtype_coderunner_testcase::setup_test_sandbox_configuration(); } @@ -48,14 +44,14 @@ protected function setUp(): void { // questiontext test code, extra, and expected for all four possible // combinations. public function test_randomised_sqr() { - + $this->check_sandbox_enabled('jobesandbox'); $iters = 0; - $tests = array( - array('searchfor' => 'print(mysqr(111))', 'answer' => "def mysqr(n): return n * n"), - array('searchfor' => 'print(mysqr(112))', 'answer' => "def mysqr(n): return n * n"), - array('searchfor' => 'print(sqr(111))', 'answer' => "def sqr(n): return n * n"), - array('searchfor' => 'print(sqr(112))', 'answer' => "def sqr(n): return n * n"), - ); + $tests = [ + ['searchfor' => 'print(mysqr(111))', 'answer' => "def mysqr(n): return n * n"], + ['searchfor' => 'print(mysqr(112))', 'answer' => "def mysqr(n): return n * n"], + ['searchfor' => 'print(sqr(111))', 'answer' => "def sqr(n): return n * n"], + ['searchfor' => 'print(sqr(112))', 'answer' => "def sqr(n): return n * n"], + ]; foreach ($tests as $test) { while ($iters < 50) { @@ -71,7 +67,7 @@ public function test_randomised_sqr() { } $this->assertTrue($iters < 50); - $this->process_submission(array('-submit' => 1, 'answer' => $test['answer'])); + $this->process_submission(['-submit' => 1, 'answer' => $test['answer']]); $this->check_current_mark(1.0); } } @@ -83,13 +79,12 @@ public function test_randomised_sqr() { // no further randomisation. public function test_randomised_sqr_with_seed() { - $iters = 0; - $tests = array( - array('searchfor' => 'print(mysqr(111))', 'answer' => "def mysqr(n): return n * n"), - array('searchfor' => 'print(mysqr(112))', 'answer' => "def mysqr(n): return n * n"), - array('searchfor' => 'print(sqr(111))', 'answer' => "def sqr(n): return n * n"), - array('searchfor' => 'print(sqr(112))', 'answer' => "def sqr(n): return n * n"), - ); + $tests = [ + ['searchfor' => 'print(mysqr(111))', 'answer' => "def mysqr(n): return n * n"], + ['searchfor' => 'print(mysqr(112))', 'answer' => "def mysqr(n): return n * n"], + ['searchfor' => 'print(sqr(111))', 'answer' => "def sqr(n): return n * n"], + ['searchfor' => 'print(sqr(112))', 'answer' => "def sqr(n): return n * n"], + ]; foreach ($tests as $test) { // First, iterate the seed until the desired situation occurs. @@ -114,13 +109,13 @@ public function test_randomised_sqr_with_seed() { $this->add_fields($q, $seed); $this->start_attempt_at_question($q, 'adaptive', 1, 1); $this->render(); - $this->assertTrue (strpos($this->currentoutput, $test['searchfor']) !== false); + $this->assertTrue(strpos($this->currentoutput, $test['searchfor']) !== false); } } } - private function add_fields($q, $seed=false) { + private function add_fields($q, $seed = false) { if ($seed !== false) { $seeding = "{{- set_random_seed($seed) -}}\n"; } else { @@ -135,12 +130,13 @@ private function add_fields($q, $seed=false) { $q->templateparamsevald = null; $q->uiparameters = null; $q->hoisttemplateparams = 1; + $q->extractcodefromjson = 1; $q->twigall = 1; $q->questiontext = 'Write a function {{ func }}'; $q->template = "{{ STUDENT_ANSWER }}\n{{ TEST.testcode }}\n{{ TEST.extra }}\n"; $q->iscombinatortemplate = false; - $q->testcases = array( - (object) array('type' => 0, + $q->testcases = [ + (object) ['type' => 0, 'testcode' => 'print({{ func }}({{ n }}))', 'extra' => 'print({{ func }}({{ n }}))', 'expected' => "{{ n * n }}\n{{ n * n }}", @@ -148,8 +144,14 @@ private function add_fields($q, $seed=false) { 'useasexample' => 1, 'display' => 'SHOW', 'mark' => 1.0, - 'hiderestiffail' => 0) - ); + 'hiderestiffail' => 0], + ]; + } + // Check if a particular sandbox is enabled. Skip test if not. + protected function check_sandbox_enabled($sandbox): void { + if (!get_config('qtype_coderunner', $sandbox . '_enabled')) { + $this->markTestSkipped("Sandbox $sandbox unavailable: test skipped"); + } } } diff --git a/tests/walkthrough_test.php b/tests/walkthrough_test.php index 7430aed11..3df3bc96f 100644 --- a/tests/walkthrough_test.php +++ b/tests/walkthrough_test.php @@ -34,17 +34,15 @@ /** * Unit tests for the coderunner question type. - * + * @coversNothing * @copyright 2011 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - - class walkthrough_test extends \qbehaviour_walkthrough_test_base { - protected function setUp(): void { parent::setUp(); \qtype_coderunner_testcase::setup_test_sandbox_configuration(); + $this->check_sandbox_enabled('jobesandbox'); } public function test_adaptive() { @@ -56,56 +54,58 @@ public function test_adaptive() { $this->check_current_state(\question_state::$todo); $this->check_current_mark(null); $this->check_current_output( - $this->get_contains_marked_out_of_summary(), - $this->get_contains_submit_button_expectation(true), - $this->get_does_not_contain_feedback_expectation(), - $this->get_does_not_contain_validation_error_expectation(), - $this->get_does_not_contain_stop_button_expectation(), - $this->get_does_not_contain_try_again_button_expectation(), - $this->get_no_hint_visible_expectation()); + $this->get_contains_marked_out_of_summary(), + $this->get_contains_submit_button_expectation(true), + $this->get_does_not_contain_feedback_expectation(), + $this->get_does_not_contain_validation_error_expectation(), + $this->get_does_not_contain_stop_button_expectation(), + $this->get_does_not_contain_try_again_button_expectation(), + $this->get_no_hint_visible_expectation() + ); $this->assertEquals('Started', $qa->summarise_action($qa->get_last_step())); // Submit blank. - $this->process_submission(array('-submit' => 1, 'answer' => '')); + $this->process_submission(['-submit' => 1, 'answer' => '']); // Verify. $this->check_current_state(\question_state::$invalid); $this->check_current_mark(null); $this->check_current_output( - $this->get_contains_marked_out_of_summary(), - $this->get_contains_submit_button_expectation(true), - $this->get_does_not_contain_feedback_expectation(), - $this->get_contains_validation_error_expectation(), - $this->get_does_not_contain_stop_button_expectation(), - $this->get_does_not_contain_try_again_button_expectation(), - $this->get_no_hint_visible_expectation()); + $this->get_contains_marked_out_of_summary(), + $this->get_contains_submit_button_expectation(true), + $this->get_does_not_contain_feedback_expectation(), + $this->get_contains_validation_error_expectation(), + $this->get_does_not_contain_stop_button_expectation(), + $this->get_does_not_contain_try_again_button_expectation(), + $this->get_no_hint_visible_expectation() + ); $this->assertEquals('Submit: ', $qa->summarise_action($qa->get_last_step())); // Submit a wrong answer. - $this->process_submission(array('-submit' => 1, 'answer' => 'def sqr(n): return n')); + $this->process_submission(['-submit' => 1, 'answer' => 'def sqr(n): return n']); // Verify. $this->check_current_state(\question_state::$todo); $this->check_current_mark(0); $this->check_current_output( - new \question_pattern_expectation('/' . + new \question_pattern_expectation('/' . preg_quote(get_string('noerrorsallowed', 'qtype_coderunner') . '/')) - ); + ); $this->assertEquals('Submit: def sqr(n): return n', $qa->summarise_action($qa->get_last_step())); // Now get it right. - $this->process_submission(array('-submit' => 1, 'answer' => 'def sqr(n): return n * n')); + $this->process_submission(['-submit' => 1, 'answer' => 'def sqr(n): return n * n']); // Verify. $this->check_current_state(\question_state::$complete); $this->check_current_mark(0.9); $this->check_current_output( - $this->get_contains_correct_expectation(), - $this->get_does_not_contain_validation_error_expectation(), - $this->get_does_not_contain_stop_button_expectation(), - $this->get_no_hint_visible_expectation()); + $this->get_contains_correct_expectation(), + $this->get_does_not_contain_validation_error_expectation(), + $this->get_does_not_contain_stop_button_expectation(), + $this->get_no_hint_visible_expectation() + ); $this->assertEquals('Submit: def sqr(n): return n * n', $qa->summarise_action($qa->get_last_step())); - } public function test_view_hidden_testcases_capability() { @@ -126,7 +126,7 @@ public function test_view_hidden_testcases_capability() { $this->start_attempt_at_question($q, 'adaptive', 1, 1); // Submit a wrong answer. - $this->process_submission(array('-submit' => 1, 'answer' => 'def sqr(n): return n')); + $this->process_submission(['-submit' => 1, 'answer' => 'def sqr(n): return n']); // This is not what we are really testing, but just to make what the test is doing clear. $this->assertTrue(has_capability('qtype/coderunner:viewhiddentestcases', $coursecontext)); @@ -138,8 +138,12 @@ public function test_view_hidden_testcases_capability() { $this->assertStringContainsString('print(sqr(-6))', $this->currentoutput); // Change users permission and check. - role_change_permission($DB->get_field('role', 'id', ['shortname' => 'editingteacher']), - $coursecontext, 'qtype/coderunner:viewhiddentestcases', CAP_PREVENT); + role_change_permission( + $DB->get_field('role', 'id', ['shortname' => 'editingteacher']), + $coursecontext, + 'qtype/coderunner:viewhiddentestcases', + CAP_PREVENT + ); $this->assertFalse(has_capability('qtype/coderunner:viewhiddentestcases', $coursecontext)); // Verify hidden cases hidden. @@ -152,34 +156,35 @@ public function test_partial_marks() { $this->start_attempt_at_question($q, 'adaptive', 1, 1); // Submit a totally wrong answer. - $this->process_submission(array('-submit' => 1, 'answer' => 'def sqr(n): return -19')); + $this->process_submission(['-submit' => 1, 'answer' => 'def sqr(n): return -19']); // Verify. $this->check_current_state(\question_state::$todo); $this->check_current_mark(0); $this->check_current_output( - new \question_pattern_expectation('/' . + new \question_pattern_expectation('/' . preg_quote(get_string('incorrect', 'question') . '/')) - ); + ); // Submit a partially right answer. - $this->process_submission(array('-submit' => 1, 'answer' => 'def sqr(n): return n * n if n < 0 else -19')); + $this->process_submission(['-submit' => 1, 'answer' => 'def sqr(n): return n * n if n < 0 else -19']); $this->check_current_mark(0.54); // 4.5/7.5 * 90%. $this->check_current_output( - new \question_pattern_expectation('/' . + new \question_pattern_expectation('/' . preg_quote(get_string('partiallycorrect', 'question') . '/')) - ); + ); // Now get it right. - $this->process_submission(array('-submit' => 1, 'answer' => 'def sqr(n): return n * n')); + $this->process_submission(['-submit' => 1, 'answer' => 'def sqr(n): return n * n']); // Verify. $this->check_current_state(\question_state::$complete); $this->check_current_mark(0.8); // Full marks but 20% penalty after 2 wrong submissions. $this->check_current_output( - $this->get_contains_correct_expectation(), - $this->get_does_not_contain_validation_error_expectation(), - $this->get_no_hint_visible_expectation()); + $this->get_contains_correct_expectation(), + $this->get_does_not_contain_validation_error_expectation(), + $this->get_no_hint_visible_expectation() + ); } @@ -207,7 +212,7 @@ public function test_behaviour_with_run_error() { $q = \test_question_maker::make_question('coderunner', 'sqr'); $this->start_attempt_at_question($q, 'adaptive', 1, 1); $answer = "def sqr(n): return n * n if n != 11 else x * x\n"; - $this->process_submission(array('-submit' => 1, 'answer' => $answer)); + $this->process_submission(['-submit' => 1, 'answer' => $answer]); $this->check_output_contains('print(sqr(0))'); $this->check_output_contains('print(sqr(1))'); $this->check_output_contains('print(sqr(11))'); @@ -239,24 +244,24 @@ public function test_grading_template_output() { $this->start_attempt_at_question($q, 'adaptive', 1, 1); // Submit a totally wrong answer. - $this->process_submission(array('-submit' => 1, 'answer' => 'def sqr(n): return -19')); + $this->process_submission(['-submit' => 1, 'answer' => 'def sqr(n): return -19']); // Verify. $this->check_current_state(\question_state::$todo); $this->check_current_mark(0); $this->check_current_output( - new \question_pattern_expectation('/' . + new \question_pattern_expectation('/' . preg_quote(get_string('incorrect', 'question') . '/')) - ); + ); // Submit a right answer - because of the broken grader it should only get 0.77 // Have to restart as the behaviour of the test system with regard to // per-submission penalties doesn't seem to work. $this->start_attempt_at_question($q, 'adaptive', 1, 1); - $this->process_submission(array('-submit' => 1, 'answer' => "def sqr(n): return n * n\n")); + $this->process_submission(['-submit' => 1, 'answer' => "def sqr(n): return n * n\n"]); $this->check_current_mark(23.0 / 31.0); - $this->check_current_output( new \question_pattern_expectation('/Tiddlypom/') ); - $this->check_current_output( new \question_pattern_expectation('/Twiddlydee/') ); + $this->check_current_output(new \question_pattern_expectation('/Tiddlypom/')); + $this->check_current_output(new \question_pattern_expectation('/Twiddlydee/')); } /* Test that if a template grader sets an abort attribute in the returned @@ -283,9 +288,9 @@ public function test_grading_template_abort() { // test cases (0, 1, 11). The sqr(11) case will be awarded zero marks // despite being given a fraction of 1.0. $this->start_attempt_at_question($q, 'adaptive', 1, 1); - $this->process_submission(array('-submit' => 1, 'answer' => "def sqr(n): return n * n\n")); + $this->process_submission(['-submit' => 1, 'answer' => "def sqr(n): return n * n\n"]); $this->check_current_mark(3.0 / 31.0); - $this->check_current_output( new \question_pattern_expectation('/Twiddlydum/') ); + $this->check_current_output(new \question_pattern_expectation('/Twiddlydum/')); } /** @@ -294,12 +299,11 @@ public function test_grading_template_abort() { public function test_result_table_sanitising() { $q = \test_question_maker::make_question('coderunner', 'sqr'); $this->start_attempt_at_question($q, 'adaptive', 1, 1); - $qa = $this->get_question_attempt(); // Submit an answer with a tag in it and make sure it's suitably // escaped so it appears in the output. - $this->process_submission(array('-submit' => 1, - 'answer' => "def sqr(n):\n print('')\n return n * n")); + $this->process_submission(['-submit' => 1, + 'answer' => "def sqr(n):\n print('')\n return n * n"]); $this->check_output_does_not_contain(''); } @@ -329,12 +333,12 @@ public function test_grading_template_html_output() { // Submit an answer that's right for all except one test case. $this->start_attempt_at_question($q, 'adaptive', 1, 1); - $this->process_submission(array('-submit' => 1, - 'answer' => "def sqr(n): return -1 if n == 1 else n * n \n")); + $this->process_submission(['-submit' => 1, + 'answer' => "def sqr(n): return -1 if n == 1 else n * n \n"]); $this->check_current_mark(21.0 / 31.0); - $this->check_current_output( new \question_pattern_expectation("||") ); - $this->check_current_output( new \question_pattern_expectation('/YeeHa/') ); - $this->check_current_output( new \question_pattern_expectation('|

Header

|') ); + $this->check_current_output(new \question_pattern_expectation("||")); + $this->check_current_output(new \question_pattern_expectation('/YeeHa/')); + $this->check_current_output(new \question_pattern_expectation('|

Header

|')); } @@ -343,8 +347,8 @@ public function test_template_debugging() { $q = \test_question_maker::make_question('coderunner', 'sqr'); $q->showsource = 1; $this->start_attempt_at_question($q, 'adaptive', 1, 1); - $this->process_submission(array('-submit' => 1, - 'answer' => 'def sqr(n): return n * n')); + $this->process_submission(['-submit' => 1, + 'answer' => 'def sqr(n): return n * n']); $this->check_output_contains('Debug: source code from all test runs'); $this->check_output_contains('Run 1'); $this->check_output_contains('SEPARATOR = "#<ab@17943918#@>#"'); @@ -360,6 +364,17 @@ public function test_hide_check() { $this->check_output_does_not_contain('Check'); } + // Check that a question with an answer preload is not gradable if answer not changed. + public function test_preload_not_graded() { + $q = \test_question_maker::make_question('coderunner', 'sqr'); + $q->answerpreload = 'def sqr(n):'; + $this->start_attempt_at_question($q, 'adaptive', 1, 1); + $this->check_output_contains('def sqr(n):'); + $this->process_submission(['-submit' => 1, 'answer' => 'def sqr(n):']); + $this->check_current_state(\question_state::$invalid); + $this->check_current_mark(null); + } + public function test_stop_button_always() { $q = \test_question_maker::make_question('coderunner', 'sqr'); $q->giveupallowed = constants::GIVEUP_ALWAYS; @@ -370,68 +385,75 @@ public function test_stop_button_always() { $this->check_current_state(\question_state::$todo); $this->check_current_mark(null); $this->check_current_output( - $this->get_contains_marked_out_of_summary(), - $this->get_contains_submit_button_expectation(true), - $this->get_does_not_contain_feedback_expectation(), - $this->get_does_not_contain_validation_error_expectation(), - $this->get_contains_stop_button_expectation(), - $this->get_does_not_contain_try_again_button_expectation(), - $this->get_no_hint_visible_expectation()); + $this->get_contains_marked_out_of_summary(), + $this->get_contains_submit_button_expectation(true), + $this->get_does_not_contain_feedback_expectation(), + $this->get_does_not_contain_validation_error_expectation(), + $this->get_contains_stop_button_expectation(), + $this->get_does_not_contain_try_again_button_expectation(), + $this->get_no_hint_visible_expectation() + ); $this->assertEquals('Started', $qa->summarise_action($qa->get_last_step())); // Submit blank. - $this->process_submission(array('-submit' => 1, 'answer' => '')); + $this->process_submission(['-submit' => 1, 'answer' => '']); // Verify. $this->check_current_state(\question_state::$invalid); $this->check_current_mark(null); $this->check_current_output( - $this->get_contains_marked_out_of_summary(), - $this->get_contains_submit_button_expectation(true), - $this->get_does_not_contain_feedback_expectation(), - $this->get_contains_validation_error_expectation(), - $this->get_contains_stop_button_expectation(), - $this->get_does_not_contain_try_again_button_expectation(), - $this->get_no_hint_visible_expectation()); + $this->get_contains_marked_out_of_summary(), + $this->get_contains_submit_button_expectation(true), + $this->get_does_not_contain_feedback_expectation(), + $this->get_contains_validation_error_expectation(), + $this->get_contains_stop_button_expectation(), + $this->get_does_not_contain_try_again_button_expectation(), + $this->get_no_hint_visible_expectation() + ); $this->assertEquals('Submit: ', $qa->summarise_action($qa->get_last_step())); // Submit a wrong answer. - $this->process_submission(array('-submit' => 1, 'answer' => 'def sqr(n): return n')); + $this->process_submission(['-submit' => 1, 'answer' => 'def sqr(n): return n']); // Verify. $this->check_current_state(\question_state::$todo); $this->check_current_mark(0); $this->check_current_output( - new \question_pattern_expectation('/' . + new \question_pattern_expectation('/' . preg_quote(get_string('noerrorsallowed', 'qtype_coderunner') . '/')), - $this->get_contains_stop_button_expectation()); + $this->get_contains_stop_button_expectation() + ); $this->assertEquals('Submit: def sqr(n): return n', $qa->summarise_action($qa->get_last_step())); // Now get it right. - $this->process_submission(array('-submit' => 1, 'answer' => 'def sqr(n): return n * n')); + $this->process_submission(['-submit' => 1, 'answer' => 'def sqr(n): return n * n']); // Verify. $this->check_current_state(\question_state::$complete); $this->check_current_mark(0.9); $this->check_current_output( - $this->get_contains_correct_expectation(), - $this->get_does_not_contain_validation_error_expectation(), - $this->get_contains_stop_button_expectation(), - $this->get_no_hint_visible_expectation()); + $this->get_contains_correct_expectation(), + $this->get_does_not_contain_validation_error_expectation(), + $this->get_contains_stop_button_expectation(), + $this->get_no_hint_visible_expectation() + ); $this->assertEquals('Submit: def sqr(n): return n * n', $qa->summarise_action($qa->get_last_step())); // Now click the Stop button. - $this->process_submission(array('-finish' => 1, 'answer' => 'def sqr(n): return n * n')); + $this->process_submission(['-finish' => 1, 'answer' => 'def sqr(n): return n * n']); $this->check_current_state(\question_state::$gradedright); $this->check_current_mark(0.9); $this->check_current_output( - $this->get_contains_correct_expectation(), - $this->get_does_not_contain_validation_error_expectation(), - $this->get_does_not_contain_stop_button_expectation(), - $this->get_no_hint_visible_expectation(), - $this->get_contains_general_feedback_expectation($q)); - $this->assertEquals('Attempt finished submitting: def sqr(n): return n * n', - $qa->summarise_action($qa->get_last_step())); + $this->get_contains_correct_expectation(), + $this->get_does_not_contain_validation_error_expectation(), + $this->get_does_not_contain_stop_button_expectation(), + $this->get_no_hint_visible_expectation(), + $this->get_contains_general_feedback_expectation($q) + ); + $this->assertEquals( + 'Attempt finished submitting: def sqr(n): return n * n', + $qa->summarise_action($qa->get_last_step()) + ); } public function test_stop_button_always_never_answered() { @@ -444,14 +466,15 @@ public function test_stop_button_always_never_answered() { $this->check_current_state(\question_state::$todo); // Click the Stop button. - $this->process_submission(array('-finish' => 1, 'answer' => '')); + $this->process_submission(['-finish' => 1, 'answer' => '']); $this->check_current_state(\question_state::$gaveup); $this->check_current_mark(0); $this->check_current_output( - $this->get_does_not_contain_validation_error_expectation(), - $this->get_does_not_contain_stop_button_expectation(), - $this->get_no_hint_visible_expectation(), - $this->get_contains_general_feedback_expectation($q)); + $this->get_does_not_contain_validation_error_expectation(), + $this->get_does_not_contain_stop_button_expectation(), + $this->get_no_hint_visible_expectation(), + $this->get_contains_general_feedback_expectation($q) + ); $this->assertEquals('Attempt finished submitting: ', $qa->summarise_action($qa->get_last_step())); // Also check what happens in Quiz deferred feedback mode, when all the quiz display @@ -460,7 +483,8 @@ public function test_stop_button_always_never_answered() { $this->displayoptions->feedback = false; $this->displayoptions->generalfeedback = false; $this->check_current_output( - $this->get_contains_general_feedback_expectation($q)); + $this->get_contains_general_feedback_expectation($q) + ); } public function test_stop_button_after_max() { @@ -473,81 +497,89 @@ public function test_stop_button_after_max() { $this->check_current_state(\question_state::$todo); $this->check_current_mark(null); $this->check_current_output( - $this->get_contains_marked_out_of_summary(), - $this->get_contains_submit_button_expectation(true), - $this->get_does_not_contain_feedback_expectation(), - $this->get_does_not_contain_validation_error_expectation(), - $this->get_does_not_contain_stop_button_expectation(), - $this->get_does_not_contain_try_again_button_expectation(), - $this->get_no_hint_visible_expectation()); + $this->get_contains_marked_out_of_summary(), + $this->get_contains_submit_button_expectation(true), + $this->get_does_not_contain_feedback_expectation(), + $this->get_does_not_contain_validation_error_expectation(), + $this->get_does_not_contain_stop_button_expectation(), + $this->get_does_not_contain_try_again_button_expectation(), + $this->get_no_hint_visible_expectation() + ); $this->assertEquals('Started', $qa->summarise_action($qa->get_last_step())); // Submit blank. - $this->process_submission(array('-submit' => 1, 'answer' => '')); + $this->process_submission(['-submit' => 1, 'answer' => '']); // Verify. $this->check_current_state(\question_state::$invalid); $this->check_current_mark(null); $this->check_current_output( - $this->get_contains_marked_out_of_summary(), - $this->get_contains_submit_button_expectation(true), - $this->get_does_not_contain_feedback_expectation(), - $this->get_contains_validation_error_expectation(), - $this->get_does_not_contain_stop_button_expectation(), - $this->get_does_not_contain_try_again_button_expectation(), - $this->get_no_hint_visible_expectation()); + $this->get_contains_marked_out_of_summary(), + $this->get_contains_submit_button_expectation(true), + $this->get_does_not_contain_feedback_expectation(), + $this->get_contains_validation_error_expectation(), + $this->get_does_not_contain_stop_button_expectation(), + $this->get_does_not_contain_try_again_button_expectation(), + $this->get_no_hint_visible_expectation() + ); $this->assertEquals('Submit: ', $qa->summarise_action($qa->get_last_step())); // Submit a wrong answer. - $this->process_submission(array('-submit' => 1, 'answer' => 'def sqr(n): return n')); + $this->process_submission(['-submit' => 1, 'answer' => 'def sqr(n): return n']); // Verify. $this->check_current_state(\question_state::$todo); $this->check_current_mark(0); $this->check_current_output( - new \question_pattern_expectation('/' . + new \question_pattern_expectation('/' . preg_quote(get_string('noerrorsallowed', 'qtype_coderunner') . '/')), - $this->get_does_not_contain_stop_button_expectation()); + $this->get_does_not_contain_stop_button_expectation() + ); $this->assertEquals('Submit: def sqr(n): return n', $qa->summarise_action($qa->get_last_step())); // Now get it right. - $this->process_submission(array('-submit' => 1, 'answer' => 'def sqr(n): return n * n')); + $this->process_submission(['-submit' => 1, 'answer' => 'def sqr(n): return n * n']); // Verify. $this->check_current_state(\question_state::$complete); $this->check_current_mark(0.9); $this->check_current_output( - $this->get_contains_correct_expectation(), - $this->get_does_not_contain_validation_error_expectation(), - $this->get_contains_stop_button_expectation(), - $this->get_no_hint_visible_expectation()); + $this->get_contains_correct_expectation(), + $this->get_does_not_contain_validation_error_expectation(), + $this->get_contains_stop_button_expectation(), + $this->get_no_hint_visible_expectation() + ); $this->assertEquals('Submit: def sqr(n): return n * n', $qa->summarise_action($qa->get_last_step())); // Submit something invalid again.. - $this->process_submission(array('-submit' => 1, 'answer' => 'wrong')); + $this->process_submission(['-submit' => 1, 'answer' => 'wrong']); // Verify. $this->check_current_state(\question_state::$complete); $this->check_current_mark(0.9); $this->check_current_output( - $this->get_contains_mark_summary(0.9), - $this->get_contains_submit_button_expectation(true), - $this->get_contains_stop_button_expectation(), - $this->get_does_not_contain_try_again_button_expectation(), - $this->get_no_hint_visible_expectation()); + $this->get_contains_mark_summary(0.9), + $this->get_contains_submit_button_expectation(true), + $this->get_contains_stop_button_expectation(), + $this->get_does_not_contain_try_again_button_expectation(), + $this->get_no_hint_visible_expectation() + ); $this->assertEquals('Submit: wrong', $qa->summarise_action($qa->get_last_step())); // Now click the Stop button. - $this->process_submission(array('-finish' => 1, 'answer' => 'wrong')); + $this->process_submission(['-finish' => 1, 'answer' => 'wrong']); $this->check_current_state(\question_state::$gradedwrong); $this->check_current_mark(0.9); $this->check_current_output( - $this->get_contains_incorrect_expectation(), - $this->get_does_not_contain_stop_button_expectation(), - $this->get_no_hint_visible_expectation(), - $this->get_contains_general_feedback_expectation($q)); - $this->assertEquals('Attempt finished submitting: wrong', - $qa->summarise_action($qa->get_last_step())); + $this->get_contains_incorrect_expectation(), + $this->get_does_not_contain_stop_button_expectation(), + $this->get_no_hint_visible_expectation(), + $this->get_contains_general_feedback_expectation($q) + ); + $this->assertEquals( + 'Attempt finished submitting: wrong', + $qa->summarise_action($qa->get_last_step()) + ); // Also check what happens in Quiz deferred feedback mode, when all the quiz display // options are false, but the question is set to override that. @@ -555,7 +587,8 @@ public function test_stop_button_after_max() { $this->displayoptions->feedback = false; $this->displayoptions->generalfeedback = false; $this->check_current_output( - $this->get_contains_general_feedback_expectation($q)); + $this->get_contains_general_feedback_expectation($q) + ); } public function test_stop_button_after_max_repeatedly_wrong() { @@ -570,45 +603,50 @@ public function test_stop_button_after_max_repeatedly_wrong() { $this->check_current_mark(null); // Submit a wrong answer. - $this->process_submission(array('-submit' => 1, 'answer' => 'def sqr(n): return n')); + $this->process_submission(['-submit' => 1, 'answer' => 'def sqr(n): return n']); // Verify. $this->check_current_state(\question_state::$todo); $this->check_current_mark(0); $this->check_current_output( - $this->get_does_not_contain_stop_button_expectation()); + $this->get_does_not_contain_stop_button_expectation() + ); // Submit a different wrong answer. - $this->process_submission(array('-submit' => 1, 'answer' => 'def sqr(n): return 2 * n')); + $this->process_submission(['-submit' => 1, 'answer' => 'def sqr(n): return 2 * n']); // Verify - now not possible to improve. $this->check_current_state(\question_state::$todo); $this->check_current_mark(0); $this->check_current_output( - $this->get_contains_correct_expectation(), - $this->get_does_not_contain_validation_error_expectation(), - $this->get_contains_stop_button_expectation(), - $this->get_no_hint_visible_expectation()); + $this->get_contains_correct_expectation(), + $this->get_does_not_contain_validation_error_expectation(), + $this->get_contains_stop_button_expectation(), + $this->get_no_hint_visible_expectation() + ); // Now click the Stop button. - $this->process_submission(array('-finish' => 1, 'answer' => 'def sqr(n): return 2 * n')); + $this->process_submission(['-finish' => 1, 'answer' => 'def sqr(n): return 2 * n']); $this->check_current_state(\question_state::$gradedwrong); $this->check_current_mark(0); $this->check_current_output( - $this->get_contains_incorrect_expectation(), - $this->get_does_not_contain_stop_button_expectation(), - $this->get_no_hint_visible_expectation(), - $this->get_contains_general_feedback_expectation($q)); - $this->assertEquals('Attempt finished submitting: def sqr(n): return 2 * n', - $qa->summarise_action($qa->get_last_step())); + $this->get_contains_incorrect_expectation(), + $this->get_does_not_contain_stop_button_expectation(), + $this->get_no_hint_visible_expectation(), + $this->get_contains_general_feedback_expectation($q) + ); + $this->assertEquals( + 'Attempt finished submitting: def sqr(n): return 2 * n', + $qa->summarise_action($qa->get_last_step()) + ); } protected function get_contains_stop_button_expectation($enabled = null): \question_contains_tag_with_attributes { - $expectedattributes = array( + $expectedattributes = [ 'type' => 'submit', 'name' => $this->quba->get_field_prefix($this->slot) . '-finish', - ); - $forbiddenattributes = array(); + ]; + $forbiddenattributes = []; if ($enabled === true) { $forbiddenattributes['disabled'] = 'disabled'; } else if ($enabled === false) { @@ -621,4 +659,11 @@ protected function get_does_not_contain_stop_button_expectation(): \question_no_ return new \question_no_pattern_expectation('/name="' . $this->quba->get_field_prefix($this->slot) . '-finish"/'); } + + // Check if a particular sandbox is enabled. Skip test if not. + protected function check_sandbox_enabled($sandbox): void { + if (!get_config('qtype_coderunner', $sandbox . '_enabled')) { + $this->markTestSkipped("Sandbox $sandbox unavailable: test skipped"); + } + } } diff --git a/vendor/composer/ClassLoader.php b/vendor/composer/ClassLoader.php index fce8549f0..da51e697b 100644 --- a/vendor/composer/ClassLoader.php +++ b/vendor/composer/ClassLoader.php @@ -43,18 +43,18 @@ class ClassLoader { // PSR-4 - private $prefixLengthsPsr4 = array(); - private $prefixDirsPsr4 = array(); - private $fallbackDirsPsr4 = array(); + private $prefixLengthsPsr4 = []; + private $prefixDirsPsr4 = []; + private $fallbackDirsPsr4 = []; // PSR-0 - private $prefixesPsr0 = array(); - private $fallbackDirsPsr0 = array(); + private $prefixesPsr0 = []; + private $fallbackDirsPsr0 = []; private $useIncludePath = false; - private $classMap = array(); + private $classMap = []; private $classMapAuthoritative = false; - private $missingClasses = array(); + private $missingClasses = []; private $apcuPrefix; public function getPrefixes() @@ -63,7 +63,7 @@ public function getPrefixes() return call_user_func_array('array_merge', $this->prefixesPsr0); } - return array(); + return []; } public function getPrefixesPsr4() @@ -299,7 +299,7 @@ public function getApcuPrefix() */ public function register($prepend = false) { - spl_autoload_register(array($this, 'loadClass'), true, $prepend); + spl_autoload_register([$this, 'loadClass'], true, $prepend); } /** @@ -307,7 +307,7 @@ public function register($prepend = false) */ public function unregister() { - spl_autoload_unregister(array($this, 'loadClass')); + spl_autoload_unregister([$this, 'loadClass']); } /** diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php index 7a91153b0..6bb2fc68f 100644 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -5,5 +5,5 @@ $vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); -return array( -); +return [ +]; diff --git a/vendor/composer/autoload_files.php b/vendor/composer/autoload_files.php index a3e38ec8b..ea581d34f 100644 --- a/vendor/composer/autoload_files.php +++ b/vendor/composer/autoload_files.php @@ -5,7 +5,7 @@ $vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); -return array( +return [ '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php', '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', -); +]; diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php index b7fc0125d..7377657da 100644 --- a/vendor/composer/autoload_namespaces.php +++ b/vendor/composer/autoload_namespaces.php @@ -5,5 +5,5 @@ $vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); -return array( -); +return [ +]; diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php index b591d7bfa..fdd651a1f 100644 --- a/vendor/composer/autoload_psr4.php +++ b/vendor/composer/autoload_psr4.php @@ -5,8 +5,8 @@ $vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); -return array( - 'Twig\\' => array($vendorDir . '/twig/twig/src'), - 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), - 'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'), -); +return [ + 'Twig\\' => [$vendorDir . '/twig/twig/src'], + 'Symfony\\Polyfill\\Mbstring\\' => [$vendorDir . '/symfony/polyfill-mbstring'], + 'Symfony\\Polyfill\\Ctype\\' => [$vendorDir . '/symfony/polyfill-ctype'], +]; diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php index 15ec51a80..a68632989 100644 --- a/vendor/composer/autoload_real.php +++ b/vendor/composer/autoload_real.php @@ -22,9 +22,9 @@ public static function getLoader() return self::$loader; } - spl_autoload_register(array('ComposerAutoloaderInita94a015043d83dd17aa9648c7dab3aaf', 'loadClassLoader'), true, true); + spl_autoload_register(['ComposerAutoloaderInita94a015043d83dd17aa9648c7dab3aaf', 'loadClassLoader'], true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(); - spl_autoload_unregister(array('ComposerAutoloaderInita94a015043d83dd17aa9648c7dab3aaf', 'loadClassLoader')); + spl_autoload_unregister(['ComposerAutoloaderInita94a015043d83dd17aa9648c7dab3aaf', 'loadClassLoader']); $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); if ($useStaticLoader) { diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index eafc921a1..2a6eeff7f 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -6,37 +6,37 @@ class ComposerStaticInita94a015043d83dd17aa9648c7dab3aaf { - public static $files = array ( + public static $files = [ '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php', '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', - ); + ]; - public static $prefixLengthsPsr4 = array ( - 'T' => - array ( + public static $prefixLengthsPsr4 = [ + 'T' => + [ 'Twig\\' => 5, - ), - 'S' => - array ( + ], + 'S' => + [ 'Symfony\\Polyfill\\Mbstring\\' => 26, 'Symfony\\Polyfill\\Ctype\\' => 23, - ), - ); + ], + ]; - public static $prefixDirsPsr4 = array ( - 'Twig\\' => - array ( + public static $prefixDirsPsr4 = [ + 'Twig\\' => + [ 0 => __DIR__ . '/..' . '/twig/twig/src', - ), - 'Symfony\\Polyfill\\Mbstring\\' => - array ( + ], + 'Symfony\\Polyfill\\Mbstring\\' => + [ 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring', - ), - 'Symfony\\Polyfill\\Ctype\\' => - array ( + ], + 'Symfony\\Polyfill\\Ctype\\' => + [ 0 => __DIR__ . '/..' . '/symfony/polyfill-ctype', - ), - ); + ], + ]; public static function getInitializer(ClassLoader $loader) { diff --git a/vendor/symfony/polyfill-ctype/bootstrap.php b/vendor/symfony/polyfill-ctype/bootstrap.php index 14d1d0faa..4eeffae11 100644 --- a/vendor/symfony/polyfill-ctype/bootstrap.php +++ b/vendor/symfony/polyfill-ctype/bootstrap.php @@ -12,15 +12,26 @@ use Symfony\Polyfill\Ctype as p; if (!function_exists('ctype_alnum')) { - function ctype_alnum($text) { return p\Ctype::ctype_alnum($text); } - function ctype_alpha($text) { return p\Ctype::ctype_alpha($text); } - function ctype_cntrl($text) { return p\Ctype::ctype_cntrl($text); } - function ctype_digit($text) { return p\Ctype::ctype_digit($text); } - function ctype_graph($text) { return p\Ctype::ctype_graph($text); } - function ctype_lower($text) { return p\Ctype::ctype_lower($text); } - function ctype_print($text) { return p\Ctype::ctype_print($text); } - function ctype_punct($text) { return p\Ctype::ctype_punct($text); } - function ctype_space($text) { return p\Ctype::ctype_space($text); } - function ctype_upper($text) { return p\Ctype::ctype_upper($text); } - function ctype_xdigit($text) { return p\Ctype::ctype_xdigit($text); } + function ctype_alnum($text) { return p\Ctype::ctype_alnum($text); + } + function ctype_alpha($text) { return p\Ctype::ctype_alpha($text); + } + function ctype_cntrl($text) { return p\Ctype::ctype_cntrl($text); + } + function ctype_digit($text) { return p\Ctype::ctype_digit($text); + } + function ctype_graph($text) { return p\Ctype::ctype_graph($text); + } + function ctype_lower($text) { return p\Ctype::ctype_lower($text); + } + function ctype_print($text) { return p\Ctype::ctype_print($text); + } + function ctype_punct($text) { return p\Ctype::ctype_punct($text); + } + function ctype_space($text) { return p\Ctype::ctype_space($text); + } + function ctype_upper($text) { return p\Ctype::ctype_upper($text); + } + function ctype_xdigit($text) { return p\Ctype::ctype_xdigit($text); + } } diff --git a/vendor/symfony/polyfill-mbstring/Mbstring.php b/vendor/symfony/polyfill-mbstring/Mbstring.php index 7bb302371..afcb96f2c 100644 --- a/vendor/symfony/polyfill-mbstring/Mbstring.php +++ b/vendor/symfony/polyfill-mbstring/Mbstring.php @@ -69,13 +69,13 @@ final class Mbstring { const MB_CASE_FOLD = PHP_INT_MAX; - private static $encodingList = array('ASCII', 'UTF-8'); + private static $encodingList = ['ASCII', 'UTF-8']; private static $language = 'neutral'; private static $internalEncoding = 'UTF-8'; - private static $caseFold = array( - array('µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"), - array('μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'), - ); + private static $caseFold = [ + ['µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"], + ['μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'], + ]; public static function mb_convert_encoding($s, $toEncoding, $fromEncoding = null) { @@ -104,7 +104,7 @@ public static function mb_convert_encoding($s, $toEncoding, $fromEncoding = null $s = iconv($fromEncoding, 'UTF-8//IGNORE', $s); } - return preg_replace_callback('/[\x80-\xFF]+/', array(__CLASS__, 'html_encoding_callback'), $s); + return preg_replace_callback('/[\x80-\xFF]+/', [__CLASS__, 'html_encoding_callback'], $s); } if ('HTML-ENTITIES' === $fromEncoding) { @@ -237,7 +237,7 @@ public static function mb_encode_numericentity($s, $convmap, $encoding = null, $ $s = iconv($encoding, 'UTF-8//IGNORE', $s); } - static $ulenMask = array("\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4); + static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; $cnt = floor(\count($convmap) / 4) * 4; $i = 0; @@ -290,7 +290,7 @@ public static function mb_convert_case($s, $mode, $encoding = null) if (null === $titleRegexp) { $titleRegexp = self::getData('titleCaseRegexp'); } - $s = preg_replace_callback($titleRegexp, array(__CLASS__, 'title_case'), $s); + $s = preg_replace_callback($titleRegexp, [__CLASS__, 'title_case'], $s); } else { if (MB_CASE_UPPER == $mode) { static $upper = null; @@ -310,7 +310,7 @@ public static function mb_convert_case($s, $mode, $encoding = null) $map = $lower; } - static $ulenMask = array("\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4); + static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; $i = 0; $len = \strlen($s); @@ -381,7 +381,7 @@ public static function mb_language($lang = null) public static function mb_list_encodings() { - return array('UTF-8'); + return ['UTF-8']; } public static function mb_encoding_aliases($encoding) @@ -389,7 +389,7 @@ public static function mb_encoding_aliases($encoding) switch (strtoupper($encoding)) { case 'UTF8': case 'UTF-8': - return array('utf8'); + return ['utf8']; } return false; @@ -404,7 +404,7 @@ public static function mb_check_encoding($var = null, $encoding = null) $encoding = self::$internalEncoding; } - return self::mb_detect_encoding($var, array($encoding)) || false !== @iconv($encoding, $encoding, $var); + return self::mb_detect_encoding($var, [$encoding]) || false !== @iconv($encoding, $encoding, $var); } public static function mb_detect_encoding($str, $encodingList = null, $strict = false) @@ -553,7 +553,7 @@ public static function mb_str_split($string, $split_length = 1, $encoding = null return preg_split($rx, $string, null, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); } - $result = array(); + $result = []; $length = mb_strlen($string, $encoding); for ($i = 0; $i < $length; $i += $split_length) { @@ -667,7 +667,7 @@ public static function mb_strstr($haystack, $needle, $part = false, $encoding = public static function mb_get_info($type = 'all') { - $info = array( + $info = [ 'internal_encoding' => self::$internalEncoding, 'http_output' => 'pass', 'http_output_conv_mimetypes' => '^(text/|application/xhtml\+xml)', @@ -682,7 +682,7 @@ public static function mb_get_info($type = 'all') 'detect_order' => self::$encodingList, 'substitute_character' => 'none', 'strict_detection' => 'Off', - ); + ]; if ('all' === $type) { return $info; diff --git a/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php b/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php index a22eca57b..012bbe244 100644 --- a/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php +++ b/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php @@ -1,6 +1,6 @@ 'a', 'B' => 'b', 'C' => 'c', @@ -1394,4 +1394,4 @@ '𞤟' => '𞥁', '𞤠' => '𞥂', '𞤡' => '𞥃', -); +]; diff --git a/vendor/symfony/polyfill-mbstring/Resources/unidata/upperCase.php b/vendor/symfony/polyfill-mbstring/Resources/unidata/upperCase.php index ecbc15895..3ba2ee677 100644 --- a/vendor/symfony/polyfill-mbstring/Resources/unidata/upperCase.php +++ b/vendor/symfony/polyfill-mbstring/Resources/unidata/upperCase.php @@ -1,6 +1,6 @@ 'A', 'b' => 'B', 'c' => 'C', @@ -1411,4 +1411,4 @@ '𞥁' => '𞤟', '𞥂' => '𞤠', '𞥃' => '𞤡', -); +]; diff --git a/vendor/symfony/polyfill-mbstring/bootstrap.php b/vendor/symfony/polyfill-mbstring/bootstrap.php index d0a93d4dd..3928be86f 100644 --- a/vendor/symfony/polyfill-mbstring/bootstrap.php +++ b/vendor/symfony/polyfill-mbstring/bootstrap.php @@ -12,124 +12,163 @@ use Symfony\Polyfill\Mbstring as p; if (!function_exists('mb_convert_encoding')) { - function mb_convert_encoding($string, $to_encoding, $from_encoding = null) { return p\Mbstring::mb_convert_encoding($string, $to_encoding, $from_encoding); } + function mb_convert_encoding($string, $to_encoding, $from_encoding = null) { return p\Mbstring::mb_convert_encoding($string, $to_encoding, $from_encoding); + } } if (!function_exists('mb_decode_mimeheader')) { - function mb_decode_mimeheader($string) { return p\Mbstring::mb_decode_mimeheader($string); } + function mb_decode_mimeheader($string) { return p\Mbstring::mb_decode_mimeheader($string); + } } if (!function_exists('mb_encode_mimeheader')) { - function mb_encode_mimeheader($string, $charset = null, $transfer_encoding = null, $newline = null, $indent = null) { return p\Mbstring::mb_encode_mimeheader($string, $charset, $transfer_encoding, $newline, $indent); } + function mb_encode_mimeheader($string, $charset = null, $transfer_encoding = null, $newline = null, $indent = null) { return p\Mbstring::mb_encode_mimeheader($string, $charset, $transfer_encoding, $newline, $indent); + } } if (!function_exists('mb_decode_numericentity')) { - function mb_decode_numericentity($string, $map, $encoding = null) { return p\Mbstring::mb_decode_numericentity($string, $map, $encoding); } + function mb_decode_numericentity($string, $map, $encoding = null) { return p\Mbstring::mb_decode_numericentity($string, $map, $encoding); + } } if (!function_exists('mb_encode_numericentity')) { - function mb_encode_numericentity($string, $map, $encoding = null, $hex = false) { return p\Mbstring::mb_encode_numericentity($string, $map, $encoding, $hex); } + function mb_encode_numericentity($string, $map, $encoding = null, $hex = false) { return p\Mbstring::mb_encode_numericentity($string, $map, $encoding, $hex); + } } if (!function_exists('mb_convert_case')) { - function mb_convert_case($string, $mode, $encoding = null) { return p\Mbstring::mb_convert_case($string, $mode, $encoding); } + function mb_convert_case($string, $mode, $encoding = null) { return p\Mbstring::mb_convert_case($string, $mode, $encoding); + } } if (!function_exists('mb_internal_encoding')) { - function mb_internal_encoding($encoding = null) { return p\Mbstring::mb_internal_encoding($encoding); } + function mb_internal_encoding($encoding = null) { return p\Mbstring::mb_internal_encoding($encoding); + } } if (!function_exists('mb_language')) { - function mb_language($language = null) { return p\Mbstring::mb_language($language); } + function mb_language($language = null) { return p\Mbstring::mb_language($language); + } } if (!function_exists('mb_list_encodings')) { - function mb_list_encodings() { return p\Mbstring::mb_list_encodings(); } + function mb_list_encodings() { return p\Mbstring::mb_list_encodings(); + } } if (!function_exists('mb_encoding_aliases')) { - function mb_encoding_aliases($encoding) { return p\Mbstring::mb_encoding_aliases($encoding); } + function mb_encoding_aliases($encoding) { return p\Mbstring::mb_encoding_aliases($encoding); + } } if (!function_exists('mb_check_encoding')) { - function mb_check_encoding($value = null, $encoding = null) { return p\Mbstring::mb_check_encoding($value, $encoding); } + function mb_check_encoding($value = null, $encoding = null) { return p\Mbstring::mb_check_encoding($value, $encoding); + } } if (!function_exists('mb_detect_encoding')) { - function mb_detect_encoding($string, $encodings = null, $strict = false) { return p\Mbstring::mb_detect_encoding($string, $encodings, $strict); } + function mb_detect_encoding($string, $encodings = null, $strict = false) { return p\Mbstring::mb_detect_encoding($string, $encodings, $strict); + } } if (!function_exists('mb_detect_order')) { - function mb_detect_order($encoding = null) { return p\Mbstring::mb_detect_order($encoding); } + function mb_detect_order($encoding = null) { return p\Mbstring::mb_detect_order($encoding); + } } if (!function_exists('mb_parse_str')) { - function mb_parse_str($string, &$result = array()) { parse_str($string, $result); } + function mb_parse_str($string, &$result = []) { parse_str($string, $result); + } } if (!function_exists('mb_strlen')) { - function mb_strlen($string, $encoding = null) { return p\Mbstring::mb_strlen($string, $encoding); } + function mb_strlen($string, $encoding = null) { return p\Mbstring::mb_strlen($string, $encoding); + } } if (!function_exists('mb_strpos')) { - function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strpos($haystack, $needle, $offset, $encoding); } + function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strpos($haystack, $needle, $offset, $encoding); + } } if (!function_exists('mb_strtolower')) { - function mb_strtolower($string, $encoding = null) { return p\Mbstring::mb_strtolower($string, $encoding); } + function mb_strtolower($string, $encoding = null) { return p\Mbstring::mb_strtolower($string, $encoding); + } } if (!function_exists('mb_strtoupper')) { - function mb_strtoupper($string, $encoding = null) { return p\Mbstring::mb_strtoupper($string, $encoding); } + function mb_strtoupper($string, $encoding = null) { return p\Mbstring::mb_strtoupper($string, $encoding); + } } if (!function_exists('mb_substitute_character')) { - function mb_substitute_character($substitute_character = null) { return p\Mbstring::mb_substitute_character($substitute_character); } + function mb_substitute_character($substitute_character = null) { return p\Mbstring::mb_substitute_character($substitute_character); + } } if (!function_exists('mb_substr')) { - function mb_substr($string, $start, $length = 2147483647, $encoding = null) { return p\Mbstring::mb_substr($string, $start, $length, $encoding); } + function mb_substr($string, $start, $length = 2147483647, $encoding = null) { return p\Mbstring::mb_substr($string, $start, $length, $encoding); + } } if (!function_exists('mb_stripos')) { - function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_stripos($haystack, $needle, $offset, $encoding); } + function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_stripos($haystack, $needle, $offset, $encoding); + } } if (!function_exists('mb_stristr')) { - function mb_stristr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_stristr($haystack, $needle, $before_needle, $encoding); } + function mb_stristr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_stristr($haystack, $needle, $before_needle, $encoding); + } } if (!function_exists('mb_strrchr')) { - function mb_strrchr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrchr($haystack, $needle, $before_needle, $encoding); } + function mb_strrchr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrchr($haystack, $needle, $before_needle, $encoding); + } } if (!function_exists('mb_strrichr')) { - function mb_strrichr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrichr($haystack, $needle, $before_needle, $encoding); } + function mb_strrichr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrichr($haystack, $needle, $before_needle, $encoding); + } } if (!function_exists('mb_strripos')) { - function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strripos($haystack, $needle, $offset, $encoding); } + function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strripos($haystack, $needle, $offset, $encoding); + } } if (!function_exists('mb_strrpos')) { - function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strrpos($haystack, $needle, $offset, $encoding); } + function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strrpos($haystack, $needle, $offset, $encoding); + } } if (!function_exists('mb_strstr')) { - function mb_strstr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strstr($haystack, $needle, $before_needle, $encoding); } + function mb_strstr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strstr($haystack, $needle, $before_needle, $encoding); + } } if (!function_exists('mb_get_info')) { - function mb_get_info($type = 'all') { return p\Mbstring::mb_get_info($type); } + function mb_get_info($type = 'all') { return p\Mbstring::mb_get_info($type); + } } if (!function_exists('mb_http_output')) { - function mb_http_output($encoding = null) { return p\Mbstring::mb_http_output($encoding); } + function mb_http_output($encoding = null) { return p\Mbstring::mb_http_output($encoding); + } } if (!function_exists('mb_strwidth')) { - function mb_strwidth($string, $encoding = null) { return p\Mbstring::mb_strwidth($string, $encoding); } + function mb_strwidth($string, $encoding = null) { return p\Mbstring::mb_strwidth($string, $encoding); + } } if (!function_exists('mb_substr_count')) { - function mb_substr_count($haystack, $needle, $encoding = null) { return p\Mbstring::mb_substr_count($haystack, $needle, $encoding); } + function mb_substr_count($haystack, $needle, $encoding = null) { return p\Mbstring::mb_substr_count($haystack, $needle, $encoding); + } } if (!function_exists('mb_output_handler')) { - function mb_output_handler($string, $status) { return p\Mbstring::mb_output_handler($string, $status); } + function mb_output_handler($string, $status) { return p\Mbstring::mb_output_handler($string, $status); + } } if (!function_exists('mb_http_input')) { - function mb_http_input($type = '') { return p\Mbstring::mb_http_input($type); } + function mb_http_input($type = '') { return p\Mbstring::mb_http_input($type); + } } if (!function_exists('mb_convert_variables')) { if (PHP_VERSION_ID >= 80000) { - function mb_convert_variables($to_encoding, $from_encoding, &$var, &...$vars) { return p\Mbstring::mb_convert_variables($to_encoding, $from_encoding, $var, ...$vars); } + function mb_convert_variables($to_encoding, $from_encoding, &$var, &...$vars) { return p\Mbstring::mb_convert_variables($to_encoding, $from_encoding, $var, ...$vars); + } } else { - function mb_convert_variables($to_encoding, $from_encoding, &...$vars) { return p\Mbstring::mb_convert_variables($to_encoding, $from_encoding, ...$vars); } + function mb_convert_variables($to_encoding, $from_encoding, &...$vars) { return p\Mbstring::mb_convert_variables($to_encoding, $from_encoding, ...$vars); + } } } if (!function_exists('mb_ord')) { - function mb_ord($string, $encoding = null) { return p\Mbstring::mb_ord($string, $encoding); } + function mb_ord($string, $encoding = null) { return p\Mbstring::mb_ord($string, $encoding); + } } if (!function_exists('mb_chr')) { - function mb_chr($codepoint, $encoding = null) { return p\Mbstring::mb_chr($codepoint, $encoding); } + function mb_chr($codepoint, $encoding = null) { return p\Mbstring::mb_chr($codepoint, $encoding); + } } if (!function_exists('mb_scrub')) { - function mb_scrub($string, $encoding = null) { $encoding = null === $encoding ? mb_internal_encoding() : $encoding; return mb_convert_encoding($string, $encoding, $encoding); } + function mb_scrub($string, $encoding = null) { $encoding = null === $encoding ? mb_internal_encoding() : $encoding; return mb_convert_encoding($string, $encoding, $encoding); + } } if (!function_exists('mb_str_split')) { - function mb_str_split($string, $length = 1, $encoding = null) { return p\Mbstring::mb_str_split($string, $length, $encoding); } + function mb_str_split($string, $length = 1, $encoding = null) { return p\Mbstring::mb_str_split($string, $length, $encoding); + } } if (extension_loaded('mbstring')) { diff --git a/vendor/twig/twig/src/ExpressionParser.php b/vendor/twig/twig/src/ExpressionParser.php index 243c7f672..a89b72e68 100644 --- a/vendor/twig/twig/src/ExpressionParser.php +++ b/vendor/twig/twig/src/ExpressionParser.php @@ -357,10 +357,10 @@ public function parseHashExpression() // a hash key can be: // - // * a number -- 12 - // * a string -- 'a' - // * a name, which is equivalent to a string -- a - // * an expression, which must be enclosed in parentheses -- (1 + 2) + // * a number -- 12 + // * a string -- 'a' + // * a name, which is equivalent to a string -- a + // * an expression, which must be enclosed in parentheses -- (1 + 2) if ($token = $stream->nextIf(/* Token::NAME_TYPE */ 5)) { $key = new ConstantExpression($token->getValue(), $token->getLine()); diff --git a/vendor/twig/twig/src/Extension/CoreExtension.php b/vendor/twig/twig/src/Extension/CoreExtension.php index 21ea62769..8c4904cdd 100644 --- a/vendor/twig/twig/src/Extension/CoreExtension.php +++ b/vendor/twig/twig/src/Extension/CoreExtension.php @@ -10,148 +10,148 @@ */ namespace Twig\Extension { -use Twig\ExpressionParser; -use Twig\Node\Expression\Binary\AddBinary; -use Twig\Node\Expression\Binary\AndBinary; -use Twig\Node\Expression\Binary\BitwiseAndBinary; -use Twig\Node\Expression\Binary\BitwiseOrBinary; -use Twig\Node\Expression\Binary\BitwiseXorBinary; -use Twig\Node\Expression\Binary\ConcatBinary; -use Twig\Node\Expression\Binary\DivBinary; -use Twig\Node\Expression\Binary\EndsWithBinary; -use Twig\Node\Expression\Binary\EqualBinary; -use Twig\Node\Expression\Binary\FloorDivBinary; -use Twig\Node\Expression\Binary\GreaterBinary; -use Twig\Node\Expression\Binary\GreaterEqualBinary; -use Twig\Node\Expression\Binary\InBinary; -use Twig\Node\Expression\Binary\LessBinary; -use Twig\Node\Expression\Binary\LessEqualBinary; -use Twig\Node\Expression\Binary\MatchesBinary; -use Twig\Node\Expression\Binary\ModBinary; -use Twig\Node\Expression\Binary\MulBinary; -use Twig\Node\Expression\Binary\NotEqualBinary; -use Twig\Node\Expression\Binary\NotInBinary; -use Twig\Node\Expression\Binary\OrBinary; -use Twig\Node\Expression\Binary\PowerBinary; -use Twig\Node\Expression\Binary\RangeBinary; -use Twig\Node\Expression\Binary\SpaceshipBinary; -use Twig\Node\Expression\Binary\StartsWithBinary; -use Twig\Node\Expression\Binary\SubBinary; -use Twig\Node\Expression\Filter\DefaultFilter; -use Twig\Node\Expression\NullCoalesceExpression; -use Twig\Node\Expression\Test\ConstantTest; -use Twig\Node\Expression\Test\DefinedTest; -use Twig\Node\Expression\Test\DivisiblebyTest; -use Twig\Node\Expression\Test\EvenTest; -use Twig\Node\Expression\Test\NullTest; -use Twig\Node\Expression\Test\OddTest; -use Twig\Node\Expression\Test\SameasTest; -use Twig\Node\Expression\Unary\NegUnary; -use Twig\Node\Expression\Unary\NotUnary; -use Twig\Node\Expression\Unary\PosUnary; -use Twig\NodeVisitor\MacroAutoImportNodeVisitor; -use Twig\TokenParser\ApplyTokenParser; -use Twig\TokenParser\BlockTokenParser; -use Twig\TokenParser\DeprecatedTokenParser; -use Twig\TokenParser\DoTokenParser; -use Twig\TokenParser\EmbedTokenParser; -use Twig\TokenParser\ExtendsTokenParser; -use Twig\TokenParser\FlushTokenParser; -use Twig\TokenParser\ForTokenParser; -use Twig\TokenParser\FromTokenParser; -use Twig\TokenParser\IfTokenParser; -use Twig\TokenParser\ImportTokenParser; -use Twig\TokenParser\IncludeTokenParser; -use Twig\TokenParser\MacroTokenParser; -use Twig\TokenParser\SetTokenParser; -use Twig\TokenParser\UseTokenParser; -use Twig\TokenParser\WithTokenParser; -use Twig\TwigFilter; -use Twig\TwigFunction; -use Twig\TwigTest; - -final class CoreExtension extends AbstractExtension -{ - private $dateFormats = ['F j, Y H:i', '%d days']; - private $numberFormat = [0, '.', ',']; - private $timezone = null; - - /** - * Sets the default format to be used by the date filter. - * - * @param string $format The default date format string - * @param string $dateIntervalFormat The default date interval format string - */ - public function setDateFormat($format = null, $dateIntervalFormat = null) + use Twig\ExpressionParser; + use Twig\Node\Expression\Binary\AddBinary; + use Twig\Node\Expression\Binary\AndBinary; + use Twig\Node\Expression\Binary\BitwiseAndBinary; + use Twig\Node\Expression\Binary\BitwiseOrBinary; + use Twig\Node\Expression\Binary\BitwiseXorBinary; + use Twig\Node\Expression\Binary\ConcatBinary; + use Twig\Node\Expression\Binary\DivBinary; + use Twig\Node\Expression\Binary\EndsWithBinary; + use Twig\Node\Expression\Binary\EqualBinary; + use Twig\Node\Expression\Binary\FloorDivBinary; + use Twig\Node\Expression\Binary\GreaterBinary; + use Twig\Node\Expression\Binary\GreaterEqualBinary; + use Twig\Node\Expression\Binary\InBinary; + use Twig\Node\Expression\Binary\LessBinary; + use Twig\Node\Expression\Binary\LessEqualBinary; + use Twig\Node\Expression\Binary\MatchesBinary; + use Twig\Node\Expression\Binary\ModBinary; + use Twig\Node\Expression\Binary\MulBinary; + use Twig\Node\Expression\Binary\NotEqualBinary; + use Twig\Node\Expression\Binary\NotInBinary; + use Twig\Node\Expression\Binary\OrBinary; + use Twig\Node\Expression\Binary\PowerBinary; + use Twig\Node\Expression\Binary\RangeBinary; + use Twig\Node\Expression\Binary\SpaceshipBinary; + use Twig\Node\Expression\Binary\StartsWithBinary; + use Twig\Node\Expression\Binary\SubBinary; + use Twig\Node\Expression\Filter\DefaultFilter; + use Twig\Node\Expression\NullCoalesceExpression; + use Twig\Node\Expression\Test\ConstantTest; + use Twig\Node\Expression\Test\DefinedTest; + use Twig\Node\Expression\Test\DivisiblebyTest; + use Twig\Node\Expression\Test\EvenTest; + use Twig\Node\Expression\Test\NullTest; + use Twig\Node\Expression\Test\OddTest; + use Twig\Node\Expression\Test\SameasTest; + use Twig\Node\Expression\Unary\NegUnary; + use Twig\Node\Expression\Unary\NotUnary; + use Twig\Node\Expression\Unary\PosUnary; + use Twig\NodeVisitor\MacroAutoImportNodeVisitor; + use Twig\TokenParser\ApplyTokenParser; + use Twig\TokenParser\BlockTokenParser; + use Twig\TokenParser\DeprecatedTokenParser; + use Twig\TokenParser\DoTokenParser; + use Twig\TokenParser\EmbedTokenParser; + use Twig\TokenParser\ExtendsTokenParser; + use Twig\TokenParser\FlushTokenParser; + use Twig\TokenParser\ForTokenParser; + use Twig\TokenParser\FromTokenParser; + use Twig\TokenParser\IfTokenParser; + use Twig\TokenParser\ImportTokenParser; + use Twig\TokenParser\IncludeTokenParser; + use Twig\TokenParser\MacroTokenParser; + use Twig\TokenParser\SetTokenParser; + use Twig\TokenParser\UseTokenParser; + use Twig\TokenParser\WithTokenParser; + use Twig\TwigFilter; + use Twig\TwigFunction; + use Twig\TwigTest; + + final class CoreExtension extends AbstractExtension { - if (null !== $format) { - $this->dateFormats[0] = $format; + private $dateFormats = ['F j, Y H:i', '%d days']; + private $numberFormat = [0, '.', ',']; + private $timezone = null; + + /** + * Sets the default format to be used by the date filter. + * + * @param string $format The default date format string + * @param string $dateIntervalFormat The default date interval format string + */ + public function setDateFormat($format = null, $dateIntervalFormat = null) + { + if (null !== $format) { + $this->dateFormats[0] = $format; + } + + if (null !== $dateIntervalFormat) { + $this->dateFormats[1] = $dateIntervalFormat; + } } - if (null !== $dateIntervalFormat) { - $this->dateFormats[1] = $dateIntervalFormat; + /** + * Gets the default format to be used by the date filter. + * + * @return array The default date format string and the default date interval format string + */ + public function getDateFormat() + { + return $this->dateFormats; } - } - /** - * Gets the default format to be used by the date filter. - * - * @return array The default date format string and the default date interval format string - */ - public function getDateFormat() - { - return $this->dateFormats; - } + /** + * Sets the default timezone to be used by the date filter. + * + * @param \DateTimeZone|string $timezone The default timezone string or a \DateTimeZone object + */ + public function setTimezone($timezone) + { + $this->timezone = $timezone instanceof \DateTimeZone ? $timezone : new \DateTimeZone($timezone); + } - /** - * Sets the default timezone to be used by the date filter. - * - * @param \DateTimeZone|string $timezone The default timezone string or a \DateTimeZone object - */ - public function setTimezone($timezone) - { - $this->timezone = $timezone instanceof \DateTimeZone ? $timezone : new \DateTimeZone($timezone); - } + /** + * Gets the default timezone to be used by the date filter. + * + * @return \DateTimeZone The default timezone currently in use + */ + public function getTimezone() + { + if (null === $this->timezone) { + $this->timezone = new \DateTimeZone(date_default_timezone_get()); + } - /** - * Gets the default timezone to be used by the date filter. - * - * @return \DateTimeZone The default timezone currently in use - */ - public function getTimezone() - { - if (null === $this->timezone) { - $this->timezone = new \DateTimeZone(date_default_timezone_get()); + return $this->timezone; } - return $this->timezone; - } - - /** - * Sets the default format to be used by the number_format filter. - * - * @param int $decimal the number of decimal places to use - * @param string $decimalPoint the character(s) to use for the decimal point - * @param string $thousandSep the character(s) to use for the thousands separator - */ - public function setNumberFormat($decimal, $decimalPoint, $thousandSep) - { - $this->numberFormat = [$decimal, $decimalPoint, $thousandSep]; - } + /** + * Sets the default format to be used by the number_format filter. + * + * @param int $decimal the number of decimal places to use + * @param string $decimalPoint the character(s) to use for the decimal point + * @param string $thousandSep the character(s) to use for the thousands separator + */ + public function setNumberFormat($decimal, $decimalPoint, $thousandSep) + { + $this->numberFormat = [$decimal, $decimalPoint, $thousandSep]; + } - /** - * Get the default format used by the number_format filter. - * - * @return array The arguments for number_format() - */ - public function getNumberFormat() - { - return $this->numberFormat; - } + /** + * Get the default format used by the number_format filter. + * + * @return array The arguments for number_format() + */ + public function getNumberFormat() + { + return $this->numberFormat; + } - public function getTokenParsers(): array - { - return [ + public function getTokenParsers(): array + { + return [ new ApplyTokenParser(), new ForTokenParser(), new IfTokenParser(), @@ -168,12 +168,12 @@ public function getTokenParsers(): array new EmbedTokenParser(), new WithTokenParser(), new DeprecatedTokenParser(), - ]; - } + ]; + } - public function getFilters(): array - { - return [ + public function getFilters(): array + { + return [ // formatting filters new TwigFilter('date', 'twig_date_format_filter', ['needs_environment' => true]), new TwigFilter('date_modify', 'twig_date_modify_filter', ['needs_environment' => true]), @@ -221,12 +221,12 @@ public function getFilters(): array // iteration and runtime new TwigFilter('default', '_twig_default_filter', ['node_class' => DefaultFilter::class]), new TwigFilter('keys', 'twig_get_array_keys_filter'), - ]; - } + ]; + } - public function getFunctions(): array - { - return [ + public function getFunctions(): array + { + return [ new TwigFunction('max', 'max'), new TwigFunction('min', 'min'), new TwigFunction('range', 'range'), @@ -236,12 +236,12 @@ public function getFunctions(): array new TwigFunction('date', 'twig_date_converter', ['needs_environment' => true]), new TwigFunction('include', 'twig_include', ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]), new TwigFunction('source', 'twig_source', ['needs_environment' => true, 'is_safe' => ['all']]), - ]; - } + ]; + } - public function getTests(): array - { - return [ + public function getTests(): array + { + return [ new TwigTest('even', null, ['node_class' => EvenTest::class]), new TwigTest('odd', null, ['node_class' => OddTest::class]), new TwigTest('defined', null, ['node_class' => DefinedTest::class]), @@ -252,17 +252,17 @@ public function getTests(): array new TwigTest('constant', null, ['node_class' => ConstantTest::class]), new TwigTest('empty', 'twig_test_empty'), new TwigTest('iterable', 'twig_test_iterable'), - ]; - } + ]; + } - public function getNodeVisitors(): array - { - return [new MacroAutoImportNodeVisitor()]; - } + public function getNodeVisitors(): array + { + return [new MacroAutoImportNodeVisitor()]; + } - public function getOperators(): array - { - return [ + public function getOperators(): array + { + return [ [ 'not' => ['precedence' => 50, 'class' => NotUnary::class], '-' => ['precedence' => 500, 'class' => NegUnary::class], @@ -299,10 +299,10 @@ public function getOperators(): array '**' => ['precedence' => 200, 'class' => PowerBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], '??' => ['precedence' => 300, 'class' => NullCoalesceExpression::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], ], - ]; + ]; + } } } -} namespace { use Twig\Environment; @@ -315,166 +315,184 @@ public function getOperators(): array use Twig\Template; use Twig\TemplateWrapper; -/** - * Cycles over a value. - * - * @param \ArrayAccess|array $values - * @param int $position The cycle position - * - * @return string The next value in the cycle - */ -function twig_cycle($values, $position) -{ - if (!\is_array($values) && !$values instanceof \ArrayAccess) { - return $values; - } - - return $values[$position % \count($values)]; -} + /** + * Cycles over a value. + * + * @param \ArrayAccess|array $values + * @param int $position The cycle position + * + * @return string The next value in the cycle + */ + function twig_cycle($values, $position) + { + if (!\is_array($values) && !$values instanceof \ArrayAccess) { + return $values; + } -/** - * Returns a random value depending on the supplied parameter type: - * - a random item from a \Traversable or array - * - a random character from a string - * - a random integer between 0 and the integer parameter. - * - * @param \Traversable|array|int|float|string $values The values to pick a random item from - * @param int|null $max Maximum value used when $values is an int - * - * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is) - * - * @return mixed A random value from the given sequence - */ -function twig_random(Environment $env, $values = null, $max = null) -{ - if (null === $values) { - return null === $max ? mt_rand() : mt_rand(0, $max); + return $values[$position % \count($values)]; } - if (\is_int($values) || \is_float($values)) { - if (null === $max) { - if ($values < 0) { - $max = 0; - $min = $values; + /** + * Returns a random value depending on the supplied parameter type: + * - a random item from a \Traversable or array + * - a random character from a string + * - a random integer between 0 and the integer parameter. + * + * @param \Traversable|array|int|float|string $values The values to pick a random item from + * @param int|null $max Maximum value used when $values is an int + * + * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is) + * + * @return mixed A random value from the given sequence + */ + function twig_random(Environment $env, $values = null, $max = null) + { + if (null === $values) { + return null === $max ? mt_rand() : mt_rand(0, $max); + } + + if (\is_int($values) || \is_float($values)) { + if (null === $max) { + if ($values < 0) { + $max = 0; + $min = $values; + } else { + $max = $values; + $min = 0; + } } else { - $max = $values; - $min = 0; + $min = $values; + $max = $max; } - } else { - $min = $values; - $max = $max; + + return mt_rand($min, $max); } - return mt_rand($min, $max); - } + if (\is_string($values)) { + if ('' === $values) { + return ''; + } - if (\is_string($values)) { - if ('' === $values) { - return ''; - } + $charset = $env->getCharset(); - $charset = $env->getCharset(); + if ('UTF-8' !== $charset) { + $values = twig_convert_encoding($values, 'UTF-8', $charset); + } - if ('UTF-8' !== $charset) { - $values = twig_convert_encoding($values, 'UTF-8', $charset); + // unicode version of str_split() + // split at all positions, but not after the start and not before the end + $values = preg_split('/(? $value) { + $values[$i] = twig_convert_encoding($value, $charset, 'UTF-8'); + } + } } - // unicode version of str_split() - // split at all positions, but not after the start and not before the end - $values = preg_split('/(? $value) { - $values[$i] = twig_convert_encoding($value, $charset, 'UTF-8'); - } + $values = twig_to_array($values); + + if (0 === \count($values)) { + throw new RuntimeError('The random function cannot pick from an empty array.'); } - } - if (!twig_test_iterable($values)) { - return $values; + return $values[array_rand($values, 1)]; } - $values = twig_to_array($values); + /** + * Converts a date to the given format. + * + * {{ post.published_at|date("m/d/Y") }} + * + * @param \DateTimeInterface|\DateInterval|string $date A date + * @param string|null $format The target format, null to use the default + * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged + * + * @return string The formatted date + */ + function twig_date_format_filter(Environment $env, $date, $format = null, $timezone = null) + { + if (null === $format) { + $formats = $env->getExtension(CoreExtension::class)->getDateFormat(); + $format = $date instanceof \DateInterval ? $formats[1] : $formats[0]; + } + + if ($date instanceof \DateInterval) { + return $date->format($format); + } - if (0 === \count($values)) { - throw new RuntimeError('The random function cannot pick from an empty array.'); + return twig_date_converter($env, $date, $timezone)->format($format); } - return $values[array_rand($values, 1)]; -} + /** + * Returns a new date object modified. + * + * {{ post.published_at|date_modify("-1day")|date("m/d/Y") }} + * + * @param \DateTimeInterface|string $date A date + * @param string $modifier A modifier string + * + * @return \DateTimeInterface + */ + function twig_date_modify_filter(Environment $env, $date, $modifier) + { + $date = twig_date_converter($env, $date, false); -/** - * Converts a date to the given format. - * - * {{ post.published_at|date("m/d/Y") }} - * - * @param \DateTimeInterface|\DateInterval|string $date A date - * @param string|null $format The target format, null to use the default - * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged - * - * @return string The formatted date - */ -function twig_date_format_filter(Environment $env, $date, $format = null, $timezone = null) -{ - if (null === $format) { - $formats = $env->getExtension(CoreExtension::class)->getDateFormat(); - $format = $date instanceof \DateInterval ? $formats[1] : $formats[0]; + return $date->modify($modifier); } - if ($date instanceof \DateInterval) { - return $date->format($format); - } + /** + * Converts an input to a \DateTime instance. + * + * {% if date(user.created_at) < date('+2days') %} + * {# do something #} + * {% endif %} + * + * @param \DateTimeInterface|string|null $date A date or null to use the current time + * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged + * + * @return \DateTimeInterface + */ + function twig_date_converter(Environment $env, $date = null, $timezone = null) + { + // determine the timezone + if (false !== $timezone) { + if (null === $timezone) { + $timezone = $env->getExtension(CoreExtension::class)->getTimezone(); + } elseif (!$timezone instanceof \DateTimeZone) { + $timezone = new \DateTimeZone($timezone); + } + } - return twig_date_converter($env, $date, $timezone)->format($format); -} + // immutable dates + if ($date instanceof \DateTimeImmutable) { + return false !== $timezone ? $date->setTimezone($timezone) : $date; + } -/** - * Returns a new date object modified. - * - * {{ post.published_at|date_modify("-1day")|date("m/d/Y") }} - * - * @param \DateTimeInterface|string $date A date - * @param string $modifier A modifier string - * - * @return \DateTimeInterface - */ -function twig_date_modify_filter(Environment $env, $date, $modifier) -{ - $date = twig_date_converter($env, $date, false); + if ($date instanceof \DateTimeInterface) { + $date = clone $date; + if (false !== $timezone) { + $date->setTimezone($timezone); + } - return $date->modify($modifier); -} + return $date; + } -/** - * Converts an input to a \DateTime instance. - * - * {% if date(user.created_at) < date('+2days') %} - * {# do something #} - * {% endif %} - * - * @param \DateTimeInterface|string|null $date A date or null to use the current time - * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged - * - * @return \DateTimeInterface - */ -function twig_date_converter(Environment $env, $date = null, $timezone = null) -{ - // determine the timezone - if (false !== $timezone) { - if (null === $timezone) { - $timezone = $env->getExtension(CoreExtension::class)->getTimezone(); - } elseif (!$timezone instanceof \DateTimeZone) { - $timezone = new \DateTimeZone($timezone); + if (null === $date || 'now' === $date) { + return new \DateTime($date, false !== $timezone ? $timezone : $env->getExtension(CoreExtension::class)->getTimezone()); } - } - // immutable dates - if ($date instanceof \DateTimeImmutable) { - return false !== $timezone ? $date->setTimezone($timezone) : $date; - } + $asString = (string) $date; + if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { + $date = new \DateTime('@'.$date); + } else { + $date = new \DateTime($date, $env->getExtension(CoreExtension::class)->getTimezone()); + } - if ($date instanceof \DateTimeInterface) { - $date = clone $date; if (false !== $timezone) { $date->setTimezone($timezone); } @@ -482,922 +500,939 @@ function twig_date_converter(Environment $env, $date = null, $timezone = null) return $date; } - if (null === $date || 'now' === $date) { - return new \DateTime($date, false !== $timezone ? $timezone : $env->getExtension(CoreExtension::class)->getTimezone()); - } + /** + * Replaces strings within a string. + * + * @param string $str String to replace in + * @param array|\Traversable $from Replace values + * + * @return string + */ + function twig_replace_filter($str, $from) + { + if (!twig_test_iterable($from)) { + throw new RuntimeError(sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from))); + } - $asString = (string) $date; - if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { - $date = new \DateTime('@'.$date); - } else { - $date = new \DateTime($date, $env->getExtension(CoreExtension::class)->getTimezone()); + return strtr($str, twig_to_array($from)); } - if (false !== $timezone) { - $date->setTimezone($timezone); - } + /** + * Rounds a number. + * + * @param int|float $value The value to round + * @param int|float $precision The rounding precision + * @param string $method The method to use for rounding + * + * @return int|float The rounded number + */ + function twig_round($value, $precision = 0, $method = 'common') + { + if ('common' === $method) { + return round($value, $precision); + } - return $date; -} + if ('ceil' !== $method && 'floor' !== $method) { + throw new RuntimeError('The round filter only supports the "common", "ceil", and "floor" methods.'); + } -/** - * Replaces strings within a string. - * - * @param string $str String to replace in - * @param array|\Traversable $from Replace values - * - * @return string - */ -function twig_replace_filter($str, $from) -{ - if (!twig_test_iterable($from)) { - throw new RuntimeError(sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from))); + return $method($value * 10 ** $precision) / 10 ** $precision; } - return strtr($str, twig_to_array($from)); -} - -/** - * Rounds a number. - * - * @param int|float $value The value to round - * @param int|float $precision The rounding precision - * @param string $method The method to use for rounding - * - * @return int|float The rounded number - */ -function twig_round($value, $precision = 0, $method = 'common') -{ - if ('common' === $method) { - return round($value, $precision); - } + /** + * Number format filter. + * + * All of the formatting options can be left null, in that case the defaults will + * be used. Supplying any of the parameters will override the defaults set in the + * environment object. + * + * @param mixed $number A float/int/string of the number to format + * @param int $decimal the number of decimal points to display + * @param string $decimalPoint the character(s) to use for the decimal point + * @param string $thousandSep the character(s) to use for the thousands separator + * + * @return string The formatted number + */ + function twig_number_format_filter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null) + { + $defaults = $env->getExtension(CoreExtension::class)->getNumberFormat(); + if (null === $decimal) { + $decimal = $defaults[0]; + } - if ('ceil' !== $method && 'floor' !== $method) { - throw new RuntimeError('The round filter only supports the "common", "ceil", and "floor" methods.'); - } + if (null === $decimalPoint) { + $decimalPoint = $defaults[1]; + } - return $method($value * 10 ** $precision) / 10 ** $precision; -} + if (null === $thousandSep) { + $thousandSep = $defaults[2]; + } -/** - * Number format filter. - * - * All of the formatting options can be left null, in that case the defaults will - * be used. Supplying any of the parameters will override the defaults set in the - * environment object. - * - * @param mixed $number A float/int/string of the number to format - * @param int $decimal the number of decimal points to display - * @param string $decimalPoint the character(s) to use for the decimal point - * @param string $thousandSep the character(s) to use for the thousands separator - * - * @return string The formatted number - */ -function twig_number_format_filter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null) -{ - $defaults = $env->getExtension(CoreExtension::class)->getNumberFormat(); - if (null === $decimal) { - $decimal = $defaults[0]; + return number_format((float) $number, $decimal, $decimalPoint, $thousandSep); } - if (null === $decimalPoint) { - $decimalPoint = $defaults[1]; - } + /** + * URL encodes (RFC 3986) a string as a path segment or an array as a query string. + * + * @param string|array $url A URL or an array of query parameters + * + * @return string The URL encoded value + */ + function twig_urlencode_filter($url) + { + if (\is_array($url)) { + return http_build_query($url, '', '&', PHP_QUERY_RFC3986); + } - if (null === $thousandSep) { - $thousandSep = $defaults[2]; + return rawurlencode($url); } - return number_format((float) $number, $decimal, $decimalPoint, $thousandSep); -} - -/** - * URL encodes (RFC 3986) a string as a path segment or an array as a query string. - * - * @param string|array $url A URL or an array of query parameters - * - * @return string The URL encoded value - */ -function twig_urlencode_filter($url) -{ - if (\is_array($url)) { - return http_build_query($url, '', '&', PHP_QUERY_RFC3986); - } + /** + * Merges an array with another one. + * + * {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %} + * + * {% set items = items|merge({ 'peugeot': 'car' }) %} + * + * {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car' } #} + * + * @param array|\Traversable $arr1 An array + * @param array|\Traversable $arr2 An array + * + * @return array The merged array + */ + function twig_array_merge($arr1, $arr2) + { + if (!twig_test_iterable($arr1)) { + throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($arr1))); + } - return rawurlencode($url); -} + if (!twig_test_iterable($arr2)) { + throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as second argument.', \gettype($arr2))); + } -/** - * Merges an array with another one. - * - * {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %} - * - * {% set items = items|merge({ 'peugeot': 'car' }) %} - * - * {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car' } #} - * - * @param array|\Traversable $arr1 An array - * @param array|\Traversable $arr2 An array - * - * @return array The merged array - */ -function twig_array_merge($arr1, $arr2) -{ - if (!twig_test_iterable($arr1)) { - throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($arr1))); + return array_merge(twig_to_array($arr1), twig_to_array($arr2)); } - if (!twig_test_iterable($arr2)) { - throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as second argument.', \gettype($arr2))); - } + /** + * Slices a variable. + * + * @param mixed $item A variable + * @param int $start Start of the slice + * @param int $length Size of the slice + * @param bool $preserveKeys Whether to preserve key or not (when the input is an array) + * + * @return mixed The sliced variable + */ + function twig_slice(Environment $env, $item, $start, $length = null, $preserveKeys = false) + { + if ($item instanceof \Traversable) { + while ($item instanceof \IteratorAggregate) { + $item = $item->getIterator(); + } - return array_merge(twig_to_array($arr1), twig_to_array($arr2)); -} + if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) { + try { + return iterator_to_array(new \LimitIterator($item, $start, null === $length ? -1 : $length), $preserveKeys); + } catch (\OutOfBoundsException $e) { + return []; + } + } -/** - * Slices a variable. - * - * @param mixed $item A variable - * @param int $start Start of the slice - * @param int $length Size of the slice - * @param bool $preserveKeys Whether to preserve key or not (when the input is an array) - * - * @return mixed The sliced variable - */ -function twig_slice(Environment $env, $item, $start, $length = null, $preserveKeys = false) -{ - if ($item instanceof \Traversable) { - while ($item instanceof \IteratorAggregate) { - $item = $item->getIterator(); + $item = iterator_to_array($item, $preserveKeys); } - if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) { - try { - return iterator_to_array(new \LimitIterator($item, $start, null === $length ? -1 : $length), $preserveKeys); - } catch (\OutOfBoundsException $e) { - return []; - } + if (\is_array($item)) { + return \array_slice($item, $start, $length, $preserveKeys); } - $item = iterator_to_array($item, $preserveKeys); - } + $item = (string) $item; - if (\is_array($item)) { - return \array_slice($item, $start, $length, $preserveKeys); + return (string) mb_substr($item, $start, $length, $env->getCharset()); } - $item = (string) $item; + /** + * Returns the first element of the item. + * + * @param mixed $item A variable + * + * @return mixed The first element of the item + */ + function twig_first(Environment $env, $item) + { + $elements = twig_slice($env, $item, 0, 1, false); - return (string) mb_substr($item, $start, $length, $env->getCharset()); -} + return \is_string($elements) ? $elements : current($elements); + } -/** - * Returns the first element of the item. - * - * @param mixed $item A variable - * - * @return mixed The first element of the item - */ -function twig_first(Environment $env, $item) -{ - $elements = twig_slice($env, $item, 0, 1, false); + /** + * Returns the last element of the item. + * + * @param mixed $item A variable + * + * @return mixed The last element of the item + */ + function twig_last(Environment $env, $item) + { + $elements = twig_slice($env, $item, -1, 1, false); - return \is_string($elements) ? $elements : current($elements); -} + return \is_string($elements) ? $elements : current($elements); + } -/** - * Returns the last element of the item. - * - * @param mixed $item A variable - * - * @return mixed The last element of the item - */ -function twig_last(Environment $env, $item) -{ - $elements = twig_slice($env, $item, -1, 1, false); - - return \is_string($elements) ? $elements : current($elements); -} + /** + * Joins the values to a string. + * + * The separators between elements are empty strings per default, you can define them with the optional parameters. + * + * {{ [1, 2, 3]|join(', ', ' and ') }} + * {# returns 1, 2 and 3 #} + * + * {{ [1, 2, 3]|join('|') }} + * {# returns 1|2|3 #} + * + * {{ [1, 2, 3]|join }} + * {# returns 123 #} + * + * @param array $value An array + * @param string $glue The separator + * @param string|null $and The separator for the last pair + * + * @return string The concatenated string + */ + function twig_join_filter($value, $glue = '', $and = null) + { + if (!twig_test_iterable($value)) { + $value = (array) $value; + } -/** - * Joins the values to a string. - * - * The separators between elements are empty strings per default, you can define them with the optional parameters. - * - * {{ [1, 2, 3]|join(', ', ' and ') }} - * {# returns 1, 2 and 3 #} - * - * {{ [1, 2, 3]|join('|') }} - * {# returns 1|2|3 #} - * - * {{ [1, 2, 3]|join }} - * {# returns 123 #} - * - * @param array $value An array - * @param string $glue The separator - * @param string|null $and The separator for the last pair - * - * @return string The concatenated string - */ -function twig_join_filter($value, $glue = '', $and = null) -{ - if (!twig_test_iterable($value)) { - $value = (array) $value; - } + $value = twig_to_array($value, false); - $value = twig_to_array($value, false); + if (0 === \count($value)) { + return ''; + } - if (0 === \count($value)) { - return ''; - } + if (null === $and || $and === $glue) { + return implode($glue, $value); + } - if (null === $and || $and === $glue) { - return implode($glue, $value); - } + if (1 === \count($value)) { + return $value[0]; + } - if (1 === \count($value)) { - return $value[0]; + return implode($glue, \array_slice($value, 0, -1)).$and.$value[\count($value) - 1]; } - return implode($glue, \array_slice($value, 0, -1)).$and.$value[\count($value) - 1]; -} + /** + * Splits the string into an array. + * + * {{ "one,two,three"|split(',') }} + * {# returns [one, two, three] #} + * + * {{ "one,two,three,four,five"|split(',', 3) }} + * {# returns [one, two, "three,four,five"] #} + * + * {{ "123"|split('') }} + * {# returns [1, 2, 3] #} + * + * {{ "aabbcc"|split('', 2) }} + * {# returns [aa, bb, cc] #} + * + * @param string $value A string + * @param string $delimiter The delimiter + * @param int $limit The limit + * + * @return array The split string as an array + */ + function twig_split_filter(Environment $env, $value, $delimiter, $limit = null) + { + if (\strlen($delimiter) > 0) { + return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit); + } -/** - * Splits the string into an array. - * - * {{ "one,two,three"|split(',') }} - * {# returns [one, two, three] #} - * - * {{ "one,two,three,four,five"|split(',', 3) }} - * {# returns [one, two, "three,four,five"] #} - * - * {{ "123"|split('') }} - * {# returns [1, 2, 3] #} - * - * {{ "aabbcc"|split('', 2) }} - * {# returns [aa, bb, cc] #} - * - * @param string $value A string - * @param string $delimiter The delimiter - * @param int $limit The limit - * - * @return array The split string as an array - */ -function twig_split_filter(Environment $env, $value, $delimiter, $limit = null) -{ - if (\strlen($delimiter) > 0) { - return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit); - } + if ($limit <= 1) { + return preg_split('/(?getCharset()); + if ($length < $limit) { + return [$value]; + } - $length = mb_strlen($value, $env->getCharset()); - if ($length < $limit) { - return [$value]; - } + $r = []; + for ($i = 0; $i < $length; $i += $limit) { + $r[] = mb_substr($value, $i, $limit, $env->getCharset()); + } - $r = []; - for ($i = 0; $i < $length; $i += $limit) { - $r[] = mb_substr($value, $i, $limit, $env->getCharset()); + return $r; } - return $r; -} + // The '_default' filter is used internally to avoid using the ternary operator + // which costs a lot for big contexts (before PHP 5.4). So, on average, + // a function call is cheaper. + /** + * @internal + */ + function _twig_default_filter($value, $default = '') + { + if (twig_test_empty($value)) { + return $default; + } -// The '_default' filter is used internally to avoid using the ternary operator -// which costs a lot for big contexts (before PHP 5.4). So, on average, -// a function call is cheaper. -/** - * @internal - */ -function _twig_default_filter($value, $default = '') -{ - if (twig_test_empty($value)) { - return $default; + return $value; } - return $value; -} + /** + * Returns the keys for the given array. + * + * It is useful when you want to iterate over the keys of an array: + * + * {% for key in array|keys %} + * {# ... #} + * {% endfor %} + * + * @param array $array An array + * + * @return array The keys + */ + function twig_get_array_keys_filter($array) + { + if ($array instanceof \Traversable) { + while ($array instanceof \IteratorAggregate) { + $array = $array->getIterator(); + } -/** - * Returns the keys for the given array. - * - * It is useful when you want to iterate over the keys of an array: - * - * {% for key in array|keys %} - * {# ... #} - * {% endfor %} - * - * @param array $array An array - * - * @return array The keys - */ -function twig_get_array_keys_filter($array) -{ - if ($array instanceof \Traversable) { - while ($array instanceof \IteratorAggregate) { - $array = $array->getIterator(); - } + if ($array instanceof \Iterator) { + $keys = []; + $array->rewind(); + while ($array->valid()) { + $keys[] = $array->key(); + $array->next(); + } + + return $keys; + } - if ($array instanceof \Iterator) { $keys = []; - $array->rewind(); - while ($array->valid()) { - $keys[] = $array->key(); - $array->next(); + foreach ($array as $key => $item) { + $keys[] = $key; } return $keys; } - $keys = []; - foreach ($array as $key => $item) { - $keys[] = $key; + if (!\is_array($array)) { + return []; } - return $keys; - } - - if (!\is_array($array)) { - return []; + return array_keys($array); } - return array_keys($array); -} + /** + * Reverses a variable. + * + * @param array|\Traversable|string $item An array, a \Traversable instance, or a string + * @param bool $preserveKeys Whether to preserve key or not + * + * @return mixed The reversed input + */ + function twig_reverse_filter(Environment $env, $item, $preserveKeys = false) + { + if ($item instanceof \Traversable) { + return array_reverse(iterator_to_array($item), $preserveKeys); + } -/** - * Reverses a variable. - * - * @param array|\Traversable|string $item An array, a \Traversable instance, or a string - * @param bool $preserveKeys Whether to preserve key or not - * - * @return mixed The reversed input - */ -function twig_reverse_filter(Environment $env, $item, $preserveKeys = false) -{ - if ($item instanceof \Traversable) { - return array_reverse(iterator_to_array($item), $preserveKeys); - } + if (\is_array($item)) { + return array_reverse($item, $preserveKeys); + } - if (\is_array($item)) { - return array_reverse($item, $preserveKeys); - } + $string = (string) $item; - $string = (string) $item; + $charset = $env->getCharset(); - $charset = $env->getCharset(); + if ('UTF-8' !== $charset) { + $item = twig_convert_encoding($string, 'UTF-8', $charset); + } - if ('UTF-8' !== $charset) { - $item = twig_convert_encoding($string, 'UTF-8', $charset); - } + preg_match_all('/./us', $item, $matches); - preg_match_all('/./us', $item, $matches); + $string = implode('', array_reverse($matches[0])); - $string = implode('', array_reverse($matches[0])); + if ('UTF-8' !== $charset) { + $string = twig_convert_encoding($string, $charset, 'UTF-8'); + } - if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, $charset, 'UTF-8'); + return $string; } - return $string; -} - -/** - * Sorts an array. - * - * @param array|\Traversable $array - * - * @return array - */ + /** + * Sorts an array. + * + * @param array|\Traversable $array + * + * @return array + */ -// *** RJL *** Add sandbox security check on the arrow function, as for -// map, reduce and filter. -function twig_sort_filter(Environment $env, $array, $arrow = null) -{ - if ($arrow !== null && !$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') + // *** RJL *** Add sandbox security check on the arrow function, as for + // map, reduce and filter. + function twig_sort_filter(Environment $env, $array, $arrow = null) + { + if ($arrow !== null && !$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) { - throw new RuntimeError('The callable passed to the "sort" filter must be a Closure in sandbox mode.'); - } - if ($array instanceof \Traversable) { - $array = iterator_to_array($array); - } elseif (!\is_array($array)) { - throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array))); - } - - if (null !== $arrow) { - uasort($array, $arrow); - } else { - asort($array); - } - - return $array; -} - -/** - * @internal - */ -function twig_in_filter($value, $compare) -{ - if ($value instanceof Markup) { - $value = (string) $value; - } - if ($compare instanceof Markup) { - $compare = (string) $compare; - } + throw new RuntimeError('The callable passed to the "sort" filter must be a Closure in sandbox mode.'); + } + if ($array instanceof \Traversable) { + $array = iterator_to_array($array); + } elseif (!\is_array($array)) { + throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array))); + } - if (\is_string($compare)) { - if (\is_string($value) || \is_int($value) || \is_float($value)) { - return '' === $value || false !== strpos($compare, (string) $value); + if (null !== $arrow) { + uasort($array, $arrow); + } else { + asort($array); } - return false; + return $array; } - if (!is_iterable($compare)) { - return false; - } + /** + * @internal + */ + function twig_in_filter($value, $compare) + { + if ($value instanceof Markup) { + $value = (string) $value; + } + if ($compare instanceof Markup) { + $compare = (string) $compare; + } - if (\is_object($value) || \is_resource($value)) { - if (!\is_array($compare)) { - foreach ($compare as $item) { - if ($item === $value) { - return true; - } + if (\is_string($compare)) { + if (\is_string($value) || \is_int($value) || \is_float($value)) { + return '' === $value || false !== strpos($compare, (string) $value); } return false; } - return \in_array($value, $compare, true); - } - - foreach ($compare as $item) { - if (0 === twig_compare($value, $item)) { - return true; + if (!is_iterable($compare)) { + return false; } - } - return false; -} + if (\is_object($value) || \is_resource($value)) { + if (!\is_array($compare)) { + foreach ($compare as $item) { + if ($item === $value) { + return true; + } + } -/** - * Compares two values using a more strict version of the PHP non-strict comparison operator. - * - * @see https://wiki.php.net/rfc/string_to_number_comparison - * @see https://wiki.php.net/rfc/trailing_whitespace_numerics - * - * @internal - */ -function twig_compare($a, $b) -{ - // int <=> string - if (\is_int($a) && \is_string($b)) { - $bTrim = trim($b, " \t\n\r\v\f"); - if (!is_numeric($bTrim)) { - return (string) $a <=> $b; - } - if ((int) $bTrim == $bTrim) { - return $a <=> (int) $bTrim; - } else { - return (float) $a <=> (float) $bTrim; - } - } - if (\is_string($a) && \is_int($b)) { - $aTrim = trim($a, " \t\n\r\v\f"); - if (!is_numeric($aTrim)) { - return $a <=> (string) $b; + return false; + } + + return \in_array($value, $compare, true); } - if ((int) $aTrim == $aTrim) { - return (int) $aTrim <=> $b; - } else { - return (float) $aTrim <=> (float) $b; + + foreach ($compare as $item) { + if (0 === twig_compare($value, $item)) { + return true; + } } + + return false; } - // float <=> string - if (\is_float($a) && \is_string($b)) { - if (is_nan($a)) { - return 1; + /** + * Compares two values using a more strict version of the PHP non-strict comparison operator. + * + * @see https://wiki.php.net/rfc/string_to_number_comparison + * @see https://wiki.php.net/rfc/trailing_whitespace_numerics + * + * @internal + */ + function twig_compare($a, $b) + { + // int <=> string + if (\is_int($a) && \is_string($b)) { + $bTrim = trim($b, " \t\n\r\v\f"); + if (!is_numeric($bTrim)) { + return (string) $a <=> $b; + } + if ((int) $bTrim == $bTrim) { + return $a <=> (int) $bTrim; + } else { + return (float) $a <=> (float) $bTrim; + } } - $bTrim = trim($b, " \t\n\r\v\f"); - if (!is_numeric($bTrim)) { - return (string) $a <=> $b; + if (\is_string($a) && \is_int($b)) { + $aTrim = trim($a, " \t\n\r\v\f"); + if (!is_numeric($aTrim)) { + return $a <=> (string) $b; + } + if ((int) $aTrim == $aTrim) { + return (int) $aTrim <=> $b; + } else { + return (float) $aTrim <=> (float) $b; + } } - return $a <=> (float) $bTrim; - } - if (\is_string($a) && \is_float($b)) { - if (is_nan($b)) { - return 1; + // float <=> string + if (\is_float($a) && \is_string($b)) { + if (is_nan($a)) { + return 1; + } + $bTrim = trim($b, " \t\n\r\v\f"); + if (!is_numeric($bTrim)) { + return (string) $a <=> $b; + } + + return $a <=> (float) $bTrim; } - $aTrim = trim($a, " \t\n\r\v\f"); - if (!is_numeric($aTrim)) { - return $a <=> (string) $b; + if (\is_string($a) && \is_float($b)) { + if (is_nan($b)) { + return 1; + } + $aTrim = trim($a, " \t\n\r\v\f"); + if (!is_numeric($aTrim)) { + return $a <=> (string) $b; + } + + return (float) $aTrim <=> $b; } - return (float) $aTrim <=> $b; + // fallback to <=> + return $a <=> $b; } - // fallback to <=> - return $a <=> $b; -} + /** + * Returns a trimmed string. + * + * @return string + * + * @throws RuntimeError When an invalid trimming side is used (not a string or not 'left', 'right', or 'both') + */ + function twig_trim_filter($string, $characterMask = null, $side = 'both') + { + if (null === $characterMask) { + $characterMask = " \t\n\r\0\x0B"; + } -/** - * Returns a trimmed string. - * - * @return string - * - * @throws RuntimeError When an invalid trimming side is used (not a string or not 'left', 'right', or 'both') - */ -function twig_trim_filter($string, $characterMask = null, $side = 'both') -{ - if (null === $characterMask) { - $characterMask = " \t\n\r\0\x0B"; + switch ($side) { + case 'both': + return trim($string, $characterMask); + case 'left': + return ltrim($string, $characterMask); + case 'right': + return rtrim($string, $characterMask); + default: + throw new RuntimeError('Trimming side must be "left", "right" or "both".'); + } } - switch ($side) { - case 'both': - return trim($string, $characterMask); - case 'left': - return ltrim($string, $characterMask); - case 'right': - return rtrim($string, $characterMask); - default: - throw new RuntimeError('Trimming side must be "left", "right" or "both".'); + /** + * Removes whitespaces between HTML tags. + * + * @return string + */ + function twig_spaceless($content) + { + return trim(preg_replace('/>\s+<', $content)); } -} -/** - * Removes whitespaces between HTML tags. - * - * @return string - */ -function twig_spaceless($content) -{ - return trim(preg_replace('/>\s+<', $content)); -} + function twig_convert_encoding($string, $to, $from) + { + if (!\function_exists('iconv')) { + throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.'); + } -function twig_convert_encoding($string, $to, $from) -{ - if (!\function_exists('iconv')) { - throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.'); + return iconv($from, $to, $string); } - return iconv($from, $to, $string); -} + /** + * Returns the length of a variable. + * + * @param mixed $thing A variable + * + * @return int The length of the value + */ + function twig_length_filter(Environment $env, $thing) + { + if (null === $thing) { + return 0; + } -/** - * Returns the length of a variable. - * - * @param mixed $thing A variable - * - * @return int The length of the value - */ -function twig_length_filter(Environment $env, $thing) -{ - if (null === $thing) { - return 0; - } + if (is_scalar($thing)) { + return mb_strlen($thing, $env->getCharset()); + } - if (is_scalar($thing)) { - return mb_strlen($thing, $env->getCharset()); - } + if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) { + return \count($thing); + } - if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) { - return \count($thing); - } + if ($thing instanceof \Traversable) { + return iterator_count($thing); + } - if ($thing instanceof \Traversable) { - return iterator_count($thing); - } + if (method_exists($thing, '__toString') && !$thing instanceof \Countable) { + return mb_strlen((string) $thing, $env->getCharset()); + } - if (method_exists($thing, '__toString') && !$thing instanceof \Countable) { - return mb_strlen((string) $thing, $env->getCharset()); + return 1; } - return 1; -} + /** + * Converts a string to uppercase. + * + * @param string $string A string + * + * @return string The uppercased string + */ + function twig_upper_filter(Environment $env, $string) + { + return mb_strtoupper($string, $env->getCharset()); + } -/** - * Converts a string to uppercase. - * - * @param string $string A string - * - * @return string The uppercased string - */ -function twig_upper_filter(Environment $env, $string) -{ - return mb_strtoupper($string, $env->getCharset()); -} + /** + * Converts a string to lowercase. + * + * @param string $string A string + * + * @return string The lowercased string + */ + function twig_lower_filter(Environment $env, $string) + { + return mb_strtolower($string, $env->getCharset()); + } -/** - * Converts a string to lowercase. - * - * @param string $string A string - * - * @return string The lowercased string - */ -function twig_lower_filter(Environment $env, $string) -{ - return mb_strtolower($string, $env->getCharset()); -} + /** + * Returns a titlecased string. + * + * @param string $string A string + * + * @return string The titlecased string + */ + function twig_title_string_filter(Environment $env, $string) + { + if (null !== $charset = $env->getCharset()) { + return mb_convert_case($string, MB_CASE_TITLE, $charset); + } -/** - * Returns a titlecased string. - * - * @param string $string A string - * - * @return string The titlecased string - */ -function twig_title_string_filter(Environment $env, $string) -{ - if (null !== $charset = $env->getCharset()) { - return mb_convert_case($string, MB_CASE_TITLE, $charset); + return ucwords(strtolower($string)); } - return ucwords(strtolower($string)); -} - -/** - * Returns a capitalized string. - * - * @param string $string A string - * - * @return string The capitalized string - */ -function twig_capitalize_string_filter(Environment $env, $string) -{ - $charset = $env->getCharset(); + /** + * Returns a capitalized string. + * + * @param string $string A string + * + * @return string The capitalized string + */ + function twig_capitalize_string_filter(Environment $env, $string) + { + $charset = $env->getCharset(); - return mb_strtoupper(mb_substr($string, 0, 1, $charset), $charset).mb_strtolower(mb_substr($string, 1, null, $charset), $charset); -} + return mb_strtoupper(mb_substr($string, 0, 1, $charset), $charset).mb_strtolower(mb_substr($string, 1, null, $charset), $charset); + } -/** - * @internal - */ -function twig_call_macro(Template $template, string $method, array $args, int $lineno, array $context, Source $source) -{ - if (!method_exists($template, $method)) { - $parent = $template; - while ($parent = $parent->getParent($context)) { - if (method_exists($parent, $method)) { - return $parent->$method(...$args); + /** + * @internal + */ + function twig_call_macro(Template $template, string $method, array $args, int $lineno, array $context, Source $source) + { + if (!method_exists($template, $method)) { + $parent = $template; + while ($parent = $parent->getParent($context)) { + if (method_exists($parent, $method)) { + return $parent->$method(...$args); + } } + + throw new RuntimeError(sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source); } - throw new RuntimeError(sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source); + return $template->$method(...$args); } - return $template->$method(...$args); -} + /** + * @internal + */ + function twig_ensure_traversable($seq) + { + if ($seq instanceof \Traversable || \is_array($seq)) { + return $seq; + } -/** - * @internal - */ -function twig_ensure_traversable($seq) -{ - if ($seq instanceof \Traversable || \is_array($seq)) { - return $seq; + return []; } - return []; -} + /** + * @internal + */ + function twig_to_array($seq, $preserveKeys = true) + { + if ($seq instanceof \Traversable) { + return iterator_to_array($seq, $preserveKeys); + } -/** - * @internal - */ -function twig_to_array($seq, $preserveKeys = true) -{ - if ($seq instanceof \Traversable) { - return iterator_to_array($seq, $preserveKeys); - } + if (!\is_array($seq)) { + return $seq; + } - if (!\is_array($seq)) { - return $seq; + return $preserveKeys ? $seq : array_values($seq); } - return $preserveKeys ? $seq : array_values($seq); -} + /** + * Checks if a variable is empty. + * + * {# evaluates to true if the foo variable is null, false, or the empty string #} + * {% if foo is empty %} + * {# ... #} + * {% endif %} + * + * @param mixed $value A variable + * + * @return bool true if the value is empty, false otherwise + */ + function twig_test_empty($value) + { + if ($value instanceof \Countable) { + return 0 === \count($value); + } -/** - * Checks if a variable is empty. - * - * {# evaluates to true if the foo variable is null, false, or the empty string #} - * {% if foo is empty %} - * {# ... #} - * {% endif %} - * - * @param mixed $value A variable - * - * @return bool true if the value is empty, false otherwise - */ -function twig_test_empty($value) -{ - if ($value instanceof \Countable) { - return 0 === \count($value); - } + if ($value instanceof \Traversable) { + return !iterator_count($value); + } - if ($value instanceof \Traversable) { - return !iterator_count($value); - } + if (\is_object($value) && method_exists($value, '__toString')) { + return '' === (string) $value; + } - if (\is_object($value) && method_exists($value, '__toString')) { - return '' === (string) $value; + return '' === $value || false === $value || null === $value || [] === $value; } - return '' === $value || false === $value || null === $value || [] === $value; -} + /** + * Checks if a variable is traversable. + * + * {# evaluates to true if the foo variable is an array or a traversable object #} + * {% if foo is iterable %} + * {# ... #} + * {% endif %} + * + * @param mixed $value A variable + * + * @return bool true if the value is traversable + */ + function twig_test_iterable($value) + { + return $value instanceof \Traversable || \is_array($value); + } -/** - * Checks if a variable is traversable. - * - * {# evaluates to true if the foo variable is an array or a traversable object #} - * {% if foo is iterable %} - * {# ... #} - * {% endif %} - * - * @param mixed $value A variable - * - * @return bool true if the value is traversable - */ -function twig_test_iterable($value) -{ - return $value instanceof \Traversable || \is_array($value); -} + /** + * Renders a template. + * + * @param array $context + * @param string|array $template The template to render or an array of templates to try consecutively + * @param array $variables The variables to pass to the template + * @param bool $withContext + * @param bool $ignoreMissing Whether to ignore missing templates or not + * @param bool $sandboxed Whether to sandbox the template or not + * + * @return string The rendered template + */ + function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false) + { + $alreadySandboxed = false; + $sandbox = null; + if ($withContext) { + $variables = array_merge($context, $variables); + } -/** - * Renders a template. - * - * @param array $context - * @param string|array $template The template to render or an array of templates to try consecutively - * @param array $variables The variables to pass to the template - * @param bool $withContext - * @param bool $ignoreMissing Whether to ignore missing templates or not - * @param bool $sandboxed Whether to sandbox the template or not - * - * @return string The rendered template - */ -function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false) -{ - $alreadySandboxed = false; - $sandbox = null; - if ($withContext) { - $variables = array_merge($context, $variables); - } + if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) { + $sandbox = $env->getExtension(SandboxExtension::class); + if (!$alreadySandboxed = $sandbox->isSandboxed()) { + $sandbox->enableSandbox(); + } - if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) { - $sandbox = $env->getExtension(SandboxExtension::class); - if (!$alreadySandboxed = $sandbox->isSandboxed()) { - $sandbox->enableSandbox(); + foreach ((\is_array($template) ? $template : [$template]) as $name) { + // if a Template instance is passed, it might have been instantiated outside of a sandbox, check security + if ($name instanceof TemplateWrapper || $name instanceof Template) { + $name->unwrap()->checkSecurity(); + } + } } - foreach ((\is_array($template) ? $template : [$template]) as $name) { - // if a Template instance is passed, it might have been instantiated outside of a sandbox, check security - if ($name instanceof TemplateWrapper || $name instanceof Template) { - $name->unwrap()->checkSecurity(); + try { + $loaded = null; + try { + $loaded = $env->resolveTemplate($template); + } catch (LoaderError $e) { + if (!$ignoreMissing) { + throw $e; + } + } + + return $loaded ? $loaded->render($variables) : ''; + } finally { + if ($isSandboxed && !$alreadySandboxed) { + $sandbox->disableSandbox(); } } } - try { - $loaded = null; + /** + * Returns a template content without rendering it. + * + * @param string $name The template name + * @param bool $ignoreMissing Whether to ignore missing templates or not + * + * @return string The template source + */ + function twig_source(Environment $env, $name, $ignoreMissing = false) + { + $loader = $env->getLoader(); try { - $loaded = $env->resolveTemplate($template); + return $loader->getSourceContext($name)->getCode(); } catch (LoaderError $e) { if (!$ignoreMissing) { throw $e; } } - - return $loaded ? $loaded->render($variables) : ''; - } finally { - if ($isSandboxed && !$alreadySandboxed) { - $sandbox->disableSandbox(); - } } -} -/** - * Returns a template content without rendering it. - * - * @param string $name The template name - * @param bool $ignoreMissing Whether to ignore missing templates or not - * - * @return string The template source - */ -function twig_source(Environment $env, $name, $ignoreMissing = false) -{ - $loader = $env->getLoader(); - try { - return $loader->getSourceContext($name)->getCode(); - } catch (LoaderError $e) { - if (!$ignoreMissing) { - throw $e; + /** + * Provides the ability to get constants from instances as well as class/global constants. + * + * @param string $constant The name of the constant + * @param object|null $object The object to get the constant from + * + * @return string + */ + function twig_constant($constant, $object = null) + { + if (null !== $object) { + $constant = \get_class($object).'::'.$constant; } - } -} -/** - * Provides the ability to get constants from instances as well as class/global constants. - * - * @param string $constant The name of the constant - * @param object|null $object The object to get the constant from - * - * @return string - */ -function twig_constant($constant, $object = null) -{ - if (null !== $object) { - $constant = \get_class($object).'::'.$constant; + return \constant($constant); } - return \constant($constant); -} + /** + * Checks if a constant exists. + * + * @param string $constant The name of the constant + * @param object|null $object The object to get the constant from + * + * @return bool + */ + function twig_constant_is_defined($constant, $object = null) + { + if (null !== $object) { + $constant = \get_class($object).'::'.$constant; + } -/** - * Checks if a constant exists. - * - * @param string $constant The name of the constant - * @param object|null $object The object to get the constant from - * - * @return bool - */ -function twig_constant_is_defined($constant, $object = null) -{ - if (null !== $object) { - $constant = \get_class($object).'::'.$constant; + return \defined($constant); } - return \defined($constant); -} - -/** - * Batches item. - * - * @param array $items An array of items - * @param int $size The size of the batch - * @param mixed $fill A value used to fill missing items - * - * @return array - */ -function twig_array_batch($items, $size, $fill = null, $preserveKeys = true) -{ - if (!twig_test_iterable($items)) { - throw new RuntimeError(sprintf('The "batch" filter expects an array or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items))); - } + /** + * Batches item. + * + * @param array $items An array of items + * @param int $size The size of the batch + * @param mixed $fill A value used to fill missing items + * + * @return array + */ + function twig_array_batch($items, $size, $fill = null, $preserveKeys = true) + { + if (!twig_test_iterable($items)) { + throw new RuntimeError(sprintf('The "batch" filter expects an array or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items))); + } - $size = ceil($size); + $size = ceil($size); - $result = array_chunk(twig_to_array($items, $preserveKeys), $size, $preserveKeys); + $result = array_chunk(twig_to_array($items, $preserveKeys), $size, $preserveKeys); - if (null !== $fill && $result) { - $last = \count($result) - 1; - if ($fillCount = $size - \count($result[$last])) { - for ($i = 0; $i < $fillCount; ++$i) { - $result[$last][] = $fill; + if (null !== $fill && $result) { + $last = \count($result) - 1; + if ($fillCount = $size - \count($result[$last])) { + for ($i = 0; $i < $fillCount; ++$i) { + $result[$last][] = $fill; + } } } - } - return $result; -} + return $result; + } -/** - * Returns the attribute value for a given array/object. - * - * @param mixed $object The object or array from where to get the item - * @param mixed $item The item to get from the array or object - * @param array $arguments An array of arguments to pass if the item is an object method - * @param string $type The type of attribute (@see \Twig\Template constants) - * @param bool $isDefinedTest Whether this is only a defined check - * @param bool $ignoreStrictCheck Whether to ignore the strict attribute check or not - * @param int $lineno The template line where the attribute was called - * - * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true - * - * @throws RuntimeError if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false - * - * @internal - */ -function twig_get_attribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = /* Template::ANY_CALL */ 'any', $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1) -{ - // array - if (/* Template::METHOD_CALL */ 'method' !== $type) { - $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; + /** + * Returns the attribute value for a given array/object. + * + * @param mixed $object The object or array from where to get the item + * @param mixed $item The item to get from the array or object + * @param array $arguments An array of arguments to pass if the item is an object method + * @param string $type The type of attribute (@see \Twig\Template constants) + * @param bool $isDefinedTest Whether this is only a defined check + * @param bool $ignoreStrictCheck Whether to ignore the strict attribute check or not + * @param int $lineno The template line where the attribute was called + * + * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true + * + * @throws RuntimeError if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false + * + * @internal + */ + function twig_get_attribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = /* Template::ANY_CALL */ 'any', $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1) + { + // array + if (/* Template::METHOD_CALL */ 'method' !== $type) { + $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; - if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object))) + if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object))) || ($object instanceof ArrayAccess && isset($object[$arrayItem])) - ) { - if ($isDefinedTest) { - return true; + ) { + if ($isDefinedTest) { + return true; + } + + return $object[$arrayItem]; } - return $object[$arrayItem]; + if (/* Template::ARRAY_CALL */ 'array' === $type || !\is_object($object)) { + if ($isDefinedTest) { + return false; + } + + if ($ignoreStrictCheck || !$env->isStrictVariables()) { + return; + } + + if ($object instanceof ArrayAccess) { + $message = sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, \get_class($object)); + } elseif (\is_object($object)) { + $message = sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object)); + } elseif (\is_array($object)) { + if (empty($object)) { + $message = sprintf('Key "%s" does not exist as the array is empty.', $arrayItem); + } else { + $message = sprintf('Key "%s" for array with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object))); + } + } elseif (/* Template::ARRAY_CALL */ 'array' === $type) { + if (null === $object) { + $message = sprintf('Impossible to access a key ("%s") on a null variable.', $item); + } else { + $message = sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + } + } elseif (null === $object) { + $message = sprintf('Impossible to access an attribute ("%s") on a null variable.', $item); + } else { + $message = sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + } + + throw new RuntimeError($message, $lineno, $source); + } } - if (/* Template::ARRAY_CALL */ 'array' === $type || !\is_object($object)) { + if (!\is_object($object)) { if ($isDefinedTest) { return false; } @@ -1406,232 +1441,198 @@ function twig_get_attribute(Environment $env, Source $source, $object, $item, ar return; } - if ($object instanceof ArrayAccess) { - $message = sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, \get_class($object)); - } elseif (\is_object($object)) { - $message = sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object)); + if (null === $object) { + $message = sprintf('Impossible to invoke a method ("%s") on a null variable.', $item); } elseif (\is_array($object)) { - if (empty($object)) { - $message = sprintf('Key "%s" does not exist as the array is empty.', $arrayItem); - } else { - $message = sprintf('Key "%s" for array with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object))); - } - } elseif (/* Template::ARRAY_CALL */ 'array' === $type) { - if (null === $object) { - $message = sprintf('Impossible to access a key ("%s") on a null variable.', $item); - } else { - $message = sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); - } - } elseif (null === $object) { - $message = sprintf('Impossible to access an attribute ("%s") on a null variable.', $item); + $message = sprintf('Impossible to invoke a method ("%s") on an array.', $item); } else { - $message = sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + $message = sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); } throw new RuntimeError($message, $lineno, $source); } - } - - if (!\is_object($object)) { - if ($isDefinedTest) { - return false; - } - - if ($ignoreStrictCheck || !$env->isStrictVariables()) { - return; - } - if (null === $object) { - $message = sprintf('Impossible to invoke a method ("%s") on a null variable.', $item); - } elseif (\is_array($object)) { - $message = sprintf('Impossible to invoke a method ("%s") on an array.', $item); - } else { - $message = sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + if ($object instanceof Template) { + throw new RuntimeError('Accessing \Twig\Template attributes is forbidden.', $lineno, $source); } - throw new RuntimeError($message, $lineno, $source); - } - - if ($object instanceof Template) { - throw new RuntimeError('Accessing \Twig\Template attributes is forbidden.', $lineno, $source); - } + // object property + if (/* Template::METHOD_CALL */ 'method' !== $type) { + if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) { + if ($isDefinedTest) { + return true; + } - // object property - if (/* Template::METHOD_CALL */ 'method' !== $type) { - if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) { - if ($isDefinedTest) { - return true; - } + if ($sandboxed) { + $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); + } - if ($sandboxed) { - $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); + return $object->$item; } - - return $object->$item; } - } - static $cache = []; - - $class = \get_class($object); - - // object method - // precedence: getXxx() > isXxx() > hasXxx() - if (!isset($cache[$class])) { - $methods = get_class_methods($object); - sort($methods); - $lcMethods = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, $methods); - $classCache = []; - foreach ($methods as $i => $method) { - $classCache[$method] = $method; - $classCache[$lcName = $lcMethods[$i]] = $method; - - if ('g' === $lcName[0] && 0 === strpos($lcName, 'get')) { - $name = substr($method, 3); - $lcName = substr($lcName, 3); - } elseif ('i' === $lcName[0] && 0 === strpos($lcName, 'is')) { - $name = substr($method, 2); - $lcName = substr($lcName, 2); - } elseif ('h' === $lcName[0] && 0 === strpos($lcName, 'has')) { - $name = substr($method, 3); - $lcName = substr($lcName, 3); - if (\in_array('is'.$lcName, $lcMethods)) { + static $cache = []; + + $class = \get_class($object); + + // object method + // precedence: getXxx() > isXxx() > hasXxx() + if (!isset($cache[$class])) { + $methods = get_class_methods($object); + sort($methods); + $lcMethods = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + }, $methods); + $classCache = []; + foreach ($methods as $i => $method) { + $classCache[$method] = $method; + $classCache[$lcName = $lcMethods[$i]] = $method; + + if ('g' === $lcName[0] && 0 === strpos($lcName, 'get')) { + $name = substr($method, 3); + $lcName = substr($lcName, 3); + } elseif ('i' === $lcName[0] && 0 === strpos($lcName, 'is')) { + $name = substr($method, 2); + $lcName = substr($lcName, 2); + } elseif ('h' === $lcName[0] && 0 === strpos($lcName, 'has')) { + $name = substr($method, 3); + $lcName = substr($lcName, 3); + if (\in_array('is'.$lcName, $lcMethods)) { + continue; + } + } else { continue; } - } else { - continue; - } - // skip get() and is() methods (in which case, $name is empty) - if ($name) { - if (!isset($classCache[$name])) { - $classCache[$name] = $method; - } + // skip get() and is() methods (in which case, $name is empty) + if ($name) { + if (!isset($classCache[$name])) { + $classCache[$name] = $method; + } - if (!isset($classCache[$lcName])) { - $classCache[$lcName] = $method; + if (!isset($classCache[$lcName])) { + $classCache[$lcName] = $method; + } } } + $cache[$class] = $classCache; } - $cache[$class] = $classCache; - } - $call = false; - if (isset($cache[$class][$item])) { - $method = $cache[$class][$item]; - } elseif (isset($cache[$class][$lcItem = strtr($item, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')])) { - $method = $cache[$class][$lcItem]; - } elseif (isset($cache[$class]['__call'])) { - $method = $item; - $call = true; - } else { - if ($isDefinedTest) { - return false; + $call = false; + if (isset($cache[$class][$item])) { + $method = $cache[$class][$item]; + } elseif (isset($cache[$class][$lcItem = strtr($item, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')])) { + $method = $cache[$class][$lcItem]; + } elseif (isset($cache[$class]['__call'])) { + $method = $item; + $call = true; + } else { + if ($isDefinedTest) { + return false; + } + + if ($ignoreStrictCheck || !$env->isStrictVariables()) { + return; + } + + throw new RuntimeError(sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source); } - if ($ignoreStrictCheck || !$env->isStrictVariables()) { - return; + if ($isDefinedTest) { + return true; } - throw new RuntimeError(sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source); - } + if ($sandboxed) { + $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source); + } - if ($isDefinedTest) { - return true; - } + // Some objects throw exceptions when they have __call, and the method we try + // to call is not supported. If ignoreStrictCheck is true, we should return null. + try { + $ret = $object->$method(...$arguments); + } catch (\BadMethodCallException $e) { + if ($call && ($ignoreStrictCheck || !$env->isStrictVariables())) { + return; + } + throw $e; + } - if ($sandboxed) { - $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source); + return $ret; } - // Some objects throw exceptions when they have __call, and the method we try - // to call is not supported. If ignoreStrictCheck is true, we should return null. - try { - $ret = $object->$method(...$arguments); - } catch (\BadMethodCallException $e) { - if ($call && ($ignoreStrictCheck || !$env->isStrictVariables())) { - return; + /** + * Returns the values from a single column in the input array. + * + *
+     *  {% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %}
+     *
+     *  {% set fruits = items|column('fruit') %}
+     *
+     *  {# fruits now contains ['apple', 'orange'] #}
+     * 
+ * + * @param array|Traversable $array An array + * @param mixed $name The column name + * @param mixed $index The column to use as the index/keys for the returned array + * + * @return array The array of values + */ + function twig_array_column($array, $name, $index = null): array + { + if ($array instanceof Traversable) { + $array = iterator_to_array($array); + } elseif (!\is_array($array)) { + throw new RuntimeError(sprintf('The column filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); } - throw $e; - } - - return $ret; -} -/** - * Returns the values from a single column in the input array. - * - *
- *  {% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %}
- *
- *  {% set fruits = items|column('fruit') %}
- *
- *  {# fruits now contains ['apple', 'orange'] #}
- * 
- * - * @param array|Traversable $array An array - * @param mixed $name The column name - * @param mixed $index The column to use as the index/keys for the returned array - * - * @return array The array of values - */ -function twig_array_column($array, $name, $index = null): array -{ - if ($array instanceof Traversable) { - $array = iterator_to_array($array); - } elseif (!\is_array($array)) { - throw new RuntimeError(sprintf('The column filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); + return array_column($array, $name, $index); } - return array_column($array, $name, $index); -} + function twig_array_filter(Environment $env, $array, $arrow) + { + if (!twig_test_iterable($array)) { + throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array))); + } -function twig_array_filter(Environment $env, $array, $arrow) -{ - if (!twig_test_iterable($array)) { - throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array))); - } + if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) { + throw new RuntimeError('The callable passed to "filter" filter must be a Closure in sandbox mode.'); + } - if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) { - throw new RuntimeError('The callable passed to "filter" filter must be a Closure in sandbox mode.'); - } + if (\is_array($array)) { + return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH); + } - if (\is_array($array)) { - return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH); + // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator + return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow); } - // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator - return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow); -} + function twig_array_map(Environment $env, $array, $arrow) + { + if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) { + throw new RuntimeError('The callable passed to the "map" filter must be a Closure in sandbox mode.'); + } -function twig_array_map(Environment $env, $array, $arrow) -{ - if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) { - throw new RuntimeError('The callable passed to the "map" filter must be a Closure in sandbox mode.'); - } + $r = []; + foreach ($array as $k => $v) { + $r[$k] = $arrow($v, $k); + } - $r = []; - foreach ($array as $k => $v) { - $r[$k] = $arrow($v, $k); + return $r; } - return $r; -} + function twig_array_reduce(Environment $env, $array, $arrow, $initial = null) + { + if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) { + throw new RuntimeError('The callable passed to the "reduce" filter must be a Closure in sandbox mode.'); + } -function twig_array_reduce(Environment $env, $array, $arrow, $initial = null) -{ - if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) { - throw new RuntimeError('The callable passed to the "reduce" filter must be a Closure in sandbox mode.'); - } + if (!\is_array($array)) { + if (!$array instanceof \Traversable) { + throw new RuntimeError(sprintf('The "reduce" filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); + } - if (!\is_array($array)) { - if (!$array instanceof \Traversable) { - throw new RuntimeError(sprintf('The "reduce" filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); + $array = iterator_to_array($array); } - $array = iterator_to_array($array); + return array_reduce($array, $arrow, $initial); } - - return array_reduce($array, $arrow, $initial); -} } diff --git a/vendor/twig/twig/src/Extension/DebugExtension.php b/vendor/twig/twig/src/Extension/DebugExtension.php index bfb23d7bd..373267282 100644 --- a/vendor/twig/twig/src/Extension/DebugExtension.php +++ b/vendor/twig/twig/src/Extension/DebugExtension.php @@ -10,55 +10,55 @@ */ namespace Twig\Extension { -use Twig\TwigFunction; + use Twig\TwigFunction; -final class DebugExtension extends AbstractExtension -{ - public function getFunctions(): array + final class DebugExtension extends AbstractExtension { - // dump is safe if var_dump is overridden by xdebug - $isDumpOutputHtmlSafe = \extension_loaded('xdebug') + public function getFunctions(): array + { + // dump is safe if var_dump is overridden by xdebug + $isDumpOutputHtmlSafe = \extension_loaded('xdebug') // false means that it was not set (and the default is on) or it explicitly enabled && (false === ini_get('xdebug.overload_var_dump') || ini_get('xdebug.overload_var_dump')) // false means that it was not set (and the default is on) or it explicitly enabled // xdebug.overload_var_dump produces HTML only when html_errors is also enabled && (false === ini_get('html_errors') || ini_get('html_errors')) || 'cli' === \PHP_SAPI - ; + ; - return [ + return [ new TwigFunction('dump', 'twig_var_dump', ['is_safe' => $isDumpOutputHtmlSafe ? ['html'] : [], 'needs_context' => true, 'needs_environment' => true, 'is_variadic' => true]), - ]; + ]; + } } } -} namespace { -use Twig\Environment; -use Twig\Template; -use Twig\TemplateWrapper; + use Twig\Environment; + use Twig\Template; + use Twig\TemplateWrapper; -function twig_var_dump(Environment $env, $context, ...$vars) -{ - if (!$env->isDebug()) { - return; - } + function twig_var_dump(Environment $env, $context, ...$vars) + { + if (!$env->isDebug()) { + return; + } - ob_start(); + ob_start(); - if (!$vars) { - $vars = []; - foreach ($context as $key => $value) { - if (!$value instanceof Template && !$value instanceof TemplateWrapper) { - $vars[$key] = $value; + if (!$vars) { + $vars = []; + foreach ($context as $key => $value) { + if (!$value instanceof Template && !$value instanceof TemplateWrapper) { + $vars[$key] = $value; + } } + + var_dump($vars); + } else { + var_dump(...$vars); } - var_dump($vars); - } else { - var_dump(...$vars); + return ob_get_clean(); } - - return ob_get_clean(); -} } diff --git a/vendor/twig/twig/src/Extension/EscaperExtension.php b/vendor/twig/twig/src/Extension/EscaperExtension.php index 642c64c91..ea3f6bcc8 100644 --- a/vendor/twig/twig/src/Extension/EscaperExtension.php +++ b/vendor/twig/twig/src/Extension/EscaperExtension.php @@ -10,208 +10,208 @@ */ namespace Twig\Extension { -use Twig\FileExtensionEscapingStrategy; -use Twig\NodeVisitor\EscaperNodeVisitor; -use Twig\TokenParser\AutoEscapeTokenParser; -use Twig\TwigFilter; + use Twig\FileExtensionEscapingStrategy; + use Twig\NodeVisitor\EscaperNodeVisitor; + use Twig\TokenParser\AutoEscapeTokenParser; + use Twig\TwigFilter; -final class EscaperExtension extends AbstractExtension -{ - private $defaultStrategy; - private $escapers = []; - - /** @internal */ - public $safeClasses = []; - - /** @internal */ - public $safeLookup = []; - - /** - * @param string|false|callable $defaultStrategy An escaping strategy - * - * @see setDefaultStrategy() - */ - public function __construct($defaultStrategy = 'html') + final class EscaperExtension extends AbstractExtension { - $this->setDefaultStrategy($defaultStrategy); - } + private $defaultStrategy; + private $escapers = []; + + /** @internal */ + public $safeClasses = []; + + /** @internal */ + public $safeLookup = []; + + /** + * @param string|false|callable $defaultStrategy An escaping strategy + * + * @see setDefaultStrategy() + */ + public function __construct($defaultStrategy = 'html') + { + $this->setDefaultStrategy($defaultStrategy); + } - public function getTokenParsers(): array - { - return [new AutoEscapeTokenParser()]; - } + public function getTokenParsers(): array + { + return [new AutoEscapeTokenParser()]; + } - public function getNodeVisitors(): array - { - return [new EscaperNodeVisitor()]; - } + public function getNodeVisitors(): array + { + return [new EscaperNodeVisitor()]; + } - public function getFilters(): array - { - return [ + public function getFilters(): array + { + return [ new TwigFilter('escape', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']), new TwigFilter('e', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']), new TwigFilter('raw', 'twig_raw_filter', ['is_safe' => ['all']]), - ]; - } - - /** - * Sets the default strategy to use when not defined by the user. - * - * The strategy can be a valid PHP callback that takes the template - * name as an argument and returns the strategy to use. - * - * @param string|false|callable $defaultStrategy An escaping strategy - */ - public function setDefaultStrategy($defaultStrategy): void - { - if ('name' === $defaultStrategy) { - $defaultStrategy = [FileExtensionEscapingStrategy::class, 'guess']; + ]; } - $this->defaultStrategy = $defaultStrategy; - } + /** + * Sets the default strategy to use when not defined by the user. + * + * The strategy can be a valid PHP callback that takes the template + * name as an argument and returns the strategy to use. + * + * @param string|false|callable $defaultStrategy An escaping strategy + */ + public function setDefaultStrategy($defaultStrategy): void + { + if ('name' === $defaultStrategy) { + $defaultStrategy = [FileExtensionEscapingStrategy::class, 'guess']; + } - /** - * Gets the default strategy to use when not defined by the user. - * - * @param string $name The template name - * - * @return string|false The default strategy to use for the template - */ - public function getDefaultStrategy(string $name) - { - // disable string callables to avoid calling a function named html or js, - // or any other upcoming escaping strategy - if (!\is_string($this->defaultStrategy) && false !== $this->defaultStrategy) { - return \call_user_func($this->defaultStrategy, $name); + $this->defaultStrategy = $defaultStrategy; } - return $this->defaultStrategy; - } + /** + * Gets the default strategy to use when not defined by the user. + * + * @param string $name The template name + * + * @return string|false The default strategy to use for the template + */ + public function getDefaultStrategy(string $name) + { + // disable string callables to avoid calling a function named html or js, + // or any other upcoming escaping strategy + if (!\is_string($this->defaultStrategy) && false !== $this->defaultStrategy) { + return \call_user_func($this->defaultStrategy, $name); + } - /** - * Defines a new escaper to be used via the escape filter. - * - * @param string $strategy The strategy name that should be used as a strategy in the escape call - * @param callable $callable A valid PHP callable - */ - public function setEscaper($strategy, callable $callable) - { - $this->escapers[$strategy] = $callable; - } + return $this->defaultStrategy; + } - /** - * Gets all defined escapers. - * - * @return callable[] An array of escapers - */ - public function getEscapers() - { - return $this->escapers; - } + /** + * Defines a new escaper to be used via the escape filter. + * + * @param string $strategy The strategy name that should be used as a strategy in the escape call + * @param callable $callable A valid PHP callable + */ + public function setEscaper($strategy, callable $callable) + { + $this->escapers[$strategy] = $callable; + } - public function setSafeClasses(array $safeClasses = []) - { - $this->safeClasses = []; - $this->safeLookup = []; - foreach ($safeClasses as $class => $strategies) { - $this->addSafeClass($class, $strategies); + /** + * Gets all defined escapers. + * + * @return callable[] An array of escapers + */ + public function getEscapers() + { + return $this->escapers; } - } - public function addSafeClass(string $class, array $strategies) - { - $class = ltrim($class, '\\'); - if (!isset($this->safeClasses[$class])) { - $this->safeClasses[$class] = []; + public function setSafeClasses(array $safeClasses = []) + { + $this->safeClasses = []; + $this->safeLookup = []; + foreach ($safeClasses as $class => $strategies) { + $this->addSafeClass($class, $strategies); + } } - $this->safeClasses[$class] = array_merge($this->safeClasses[$class], $strategies); - foreach ($strategies as $strategy) { - $this->safeLookup[$strategy][$class] = true; + public function addSafeClass(string $class, array $strategies) + { + $class = ltrim($class, '\\'); + if (!isset($this->safeClasses[$class])) { + $this->safeClasses[$class] = []; + } + $this->safeClasses[$class] = array_merge($this->safeClasses[$class], $strategies); + + foreach ($strategies as $strategy) { + $this->safeLookup[$strategy][$class] = true; + } } } } -} namespace { -use Twig\Environment; -use Twig\Error\RuntimeError; -use Twig\Extension\EscaperExtension; -use Twig\Markup; -use Twig\Node\Expression\ConstantExpression; -use Twig\Node\Node; - -/** - * Marks a variable as being safe. - * - * @param string $string A PHP variable - */ -function twig_raw_filter($string) -{ - return $string; -} + use Twig\Environment; + use Twig\Error\RuntimeError; + use Twig\Extension\EscaperExtension; + use Twig\Markup; + use Twig\Node\Expression\ConstantExpression; + use Twig\Node\Node; -/** - * Escapes a string. - * - * @param mixed $string The value to be escaped - * @param string $strategy The escaping strategy - * @param string $charset The charset - * @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false) - * - * @return string - */ -function twig_escape_filter(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false) -{ - if ($autoescape && $string instanceof Markup) { + /** + * Marks a variable as being safe. + * + * @param string $string A PHP variable + */ + function twig_raw_filter($string) + { return $string; } - if (!\is_string($string)) { - if (\is_object($string) && method_exists($string, '__toString')) { - if ($autoescape) { - $c = \get_class($string); - $ext = $env->getExtension(EscaperExtension::class); - if (!isset($ext->safeClasses[$c])) { - $ext->safeClasses[$c] = []; - foreach (class_parents($string) + class_implements($string) as $class) { - if (isset($ext->safeClasses[$class])) { - $ext->safeClasses[$c] = array_unique(array_merge($ext->safeClasses[$c], $ext->safeClasses[$class])); - foreach ($ext->safeClasses[$class] as $s) { - $ext->safeLookup[$s][$c] = true; + /** + * Escapes a string. + * + * @param mixed $string The value to be escaped + * @param string $strategy The escaping strategy + * @param string $charset The charset + * @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false) + * + * @return string + */ + function twig_escape_filter(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false) + { + if ($autoescape && $string instanceof Markup) { + return $string; + } + + if (!\is_string($string)) { + if (\is_object($string) && method_exists($string, '__toString')) { + if ($autoescape) { + $c = \get_class($string); + $ext = $env->getExtension(EscaperExtension::class); + if (!isset($ext->safeClasses[$c])) { + $ext->safeClasses[$c] = []; + foreach (class_parents($string) + class_implements($string) as $class) { + if (isset($ext->safeClasses[$class])) { + $ext->safeClasses[$c] = array_unique(array_merge($ext->safeClasses[$c], $ext->safeClasses[$class])); + foreach ($ext->safeClasses[$class] as $s) { + $ext->safeLookup[$s][$c] = true; + } } } } + if (isset($ext->safeLookup[$strategy][$c]) || isset($ext->safeLookup['all'][$c])) { + return (string) $string; + } } - if (isset($ext->safeLookup[$strategy][$c]) || isset($ext->safeLookup['all'][$c])) { - return (string) $string; - } - } - $string = (string) $string; - } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) { - return $string; + $string = (string) $string; + } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) { + return $string; + } } - } - if ('' === $string) { - return ''; - } + if ('' === $string) { + return ''; + } - if (null === $charset) { - $charset = $env->getCharset(); - } + if (null === $charset) { + $charset = $env->getCharset(); + } - switch ($strategy) { - case 'html': - // see https://secure.php.net/htmlspecialchars + switch ($strategy) { + case 'html': + // see https://secure.php.net/htmlspecialchars - // Using a static variable to avoid initializing the array - // each time the function is called. Moving the declaration on the - // top of the function slow downs other escaping strategies. - static $htmlspecialcharsCharsets = [ + // Using a static variable to avoid initializing the array + // each time the function is called. Moving the declaration on the + // top of the function slow downs other escaping strategies. + static $htmlspecialcharsCharsets = [ 'ISO-8859-1' => true, 'ISO8859-1' => true, 'ISO-8859-15' => true, 'ISO8859-15' => true, 'utf-8' => true, 'UTF-8' => true, @@ -226,44 +226,44 @@ function twig_escape_filter(Environment $env, $string, $strategy = 'html', $char 'SHIFT_JIS' => true, 'SJIS' => true, '932' => true, 'EUC-JP' => true, 'EUCJP' => true, 'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true, - ]; + ]; - if (isset($htmlspecialcharsCharsets[$charset])) { - return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset); - } + if (isset($htmlspecialcharsCharsets[$charset])) { + return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset); + } - if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) { - // cache the lowercase variant for future iterations - $htmlspecialcharsCharsets[$charset] = true; + if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) { + // cache the lowercase variant for future iterations + $htmlspecialcharsCharsets[$charset] = true; - return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset); - } + return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset); + } - $string = twig_convert_encoding($string, 'UTF-8', $charset); - $string = htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + $string = twig_convert_encoding($string, 'UTF-8', $charset); + $string = htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); - return iconv('UTF-8', $charset, $string); + return iconv('UTF-8', $charset, $string); - case 'js': - // escape all non-alphanumeric characters - // into their \x or \uHHHH representations - if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, 'UTF-8', $charset); - } + case 'js': + // escape all non-alphanumeric characters + // into their \x or \uHHHH representations + if ('UTF-8' !== $charset) { + $string = twig_convert_encoding($string, 'UTF-8', $charset); + } - if (!preg_match('//u', $string)) { - throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); - } + if (!preg_match('//u', $string)) { + throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); + } - $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) { - $char = $matches[0]; + $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) { + $char = $matches[0]; - /* - * A few characters have short escape sequences in JSON and JavaScript. - * Escape sequences supported only by JavaScript, not JSON, are omitted. - * \" is also supported but omitted, because the resulting string is not HTML safe. - */ - static $shortMap = [ + /* + * A few characters have short escape sequences in JSON and JavaScript. + * Escape sequences supported only by JavaScript, not JSON, are omitted. + * \" is also supported but omitted, because the resulting string is not HTML safe. + */ + static $shortMap = [ '\\' => '\\\\', '/' => '\\/', "\x08" => '\b', @@ -271,151 +271,151 @@ function twig_escape_filter(Environment $env, $string, $strategy = 'html', $char "\x0A" => '\n', "\x0D" => '\r', "\x09" => '\t', - ]; - - if (isset($shortMap[$char])) { - return $shortMap[$char]; - } + ]; - $codepoint = mb_ord($char); - if (0x10000 > $codepoint) { - return sprintf('\u%04X', $codepoint); - } + if (isset($shortMap[$char])) { + return $shortMap[$char]; + } - // Split characters outside the BMP into surrogate pairs - // https://tools.ietf.org/html/rfc2781.html#section-2.1 - $u = $codepoint - 0x10000; - $high = 0xD800 | ($u >> 10); - $low = 0xDC00 | ($u & 0x3FF); + $codepoint = mb_ord($char); + if (0x10000 > $codepoint) { + return sprintf('\u%04X', $codepoint); + } - return sprintf('\u%04X\u%04X', $high, $low); - }, $string); + // Split characters outside the BMP into surrogate pairs + // https://tools.ietf.org/html/rfc2781.html#section-2.1 + $u = $codepoint - 0x10000; + $high = 0xD800 | ($u >> 10); + $low = 0xDC00 | ($u & 0x3FF); - if ('UTF-8' !== $charset) { - $string = iconv('UTF-8', $charset, $string); - } + return sprintf('\u%04X\u%04X', $high, $low); + }, $string); - return $string; + if ('UTF-8' !== $charset) { + $string = iconv('UTF-8', $charset, $string); + } - case 'css': - if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, 'UTF-8', $charset); - } + return $string; - if (!preg_match('//u', $string)) { - throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); - } + case 'css': + if ('UTF-8' !== $charset) { + $string = twig_convert_encoding($string, 'UTF-8', $charset); + } - $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) { - $char = $matches[0]; + if (!preg_match('//u', $string)) { + throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); + } - return sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8')); - }, $string); + $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) { + $char = $matches[0]; - if ('UTF-8' !== $charset) { - $string = iconv('UTF-8', $charset, $string); - } + return sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8')); + }, $string); - return $string; + if ('UTF-8' !== $charset) { + $string = iconv('UTF-8', $charset, $string); + } - case 'html_attr': - if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, 'UTF-8', $charset); - } + return $string; - if (!preg_match('//u', $string)) { - throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); - } + case 'html_attr': + if ('UTF-8' !== $charset) { + $string = twig_convert_encoding($string, 'UTF-8', $charset); + } - $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) { - /** - * This function is adapted from code coming from Zend Framework. - * - * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com) - * @license https://framework.zend.com/license/new-bsd New BSD License - */ - $chr = $matches[0]; - $ord = \ord($chr); - - /* - * The following replaces characters undefined in HTML with the - * hex entity for the Unicode replacement character. - */ - if (($ord <= 0x1f && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7f && $ord <= 0x9f)) { - return '�'; + if (!preg_match('//u', $string)) { + throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); } - /* - * Check if the current character to escape has a name entity we should - * replace it with while grabbing the hex value of the character. - */ - if (1 === \strlen($chr)) { + $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) { + /** + * This function is adapted from code coming from Zend Framework. + * + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com) + * @license https://framework.zend.com/license/new-bsd New BSD License + */ + $chr = $matches[0]; + $ord = \ord($chr); + + /* + * The following replaces characters undefined in HTML with the + * hex entity for the Unicode replacement character. + */ + if (($ord <= 0x1f && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7f && $ord <= 0x9f)) { + return '�'; + } + /* - * While HTML supports far more named entities, the lowest common denominator - * has become HTML5's XML Serialisation which is restricted to the those named - * entities that XML supports. Using HTML entities would result in this error: - * XML Parsing Error: undefined entity + * Check if the current character to escape has a name entity we should + * replace it with while grabbing the hex value of the character. */ - static $entityMap = [ + if (1 === \strlen($chr)) { + /* + * While HTML supports far more named entities, the lowest common denominator + * has become HTML5's XML Serialisation which is restricted to the those named + * entities that XML supports. Using HTML entities would result in this error: + * XML Parsing Error: undefined entity + */ + static $entityMap = [ 34 => '"', /* quotation mark */ 38 => '&', /* ampersand */ 60 => '<', /* less-than sign */ 62 => '>', /* greater-than sign */ - ]; + ]; - if (isset($entityMap[$ord])) { - return $entityMap[$ord]; - } + if (isset($entityMap[$ord])) { + return $entityMap[$ord]; + } - return sprintf('&#x%02X;', $ord); - } + return sprintf('&#x%02X;', $ord); + } - /* - * Per OWASP recommendations, we'll use hex entities for any other - * characters where a named entity does not exist. - */ - return sprintf('&#x%04X;', mb_ord($chr, 'UTF-8')); - }, $string); + /* + * Per OWASP recommendations, we'll use hex entities for any other + * characters where a named entity does not exist. + */ + return sprintf('&#x%04X;', mb_ord($chr, 'UTF-8')); + }, $string); - if ('UTF-8' !== $charset) { - $string = iconv('UTF-8', $charset, $string); - } + if ('UTF-8' !== $charset) { + $string = iconv('UTF-8', $charset, $string); + } - return $string; + return $string; - case 'url': - return rawurlencode($string); + case 'url': + return rawurlencode($string); - default: - static $escapers; + default: + static $escapers; - if (null === $escapers) { - $escapers = $env->getExtension(EscaperExtension::class)->getEscapers(); - } + if (null === $escapers) { + $escapers = $env->getExtension(EscaperExtension::class)->getEscapers(); + } - if (isset($escapers[$strategy])) { - return $escapers[$strategy]($env, $string, $charset); - } + if (isset($escapers[$strategy])) { + return $escapers[$strategy]($env, $string, $charset); + } - $validStrategies = implode(', ', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers))); + $validStrategies = implode(', ', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers))); - throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies)); + throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies)); + } } -} -/** - * @internal - */ -function twig_escape_filter_is_safe(Node $filterArgs) -{ - foreach ($filterArgs as $arg) { - if ($arg instanceof ConstantExpression) { - return [$arg->getAttribute('value')]; + /** + * @internal + */ + function twig_escape_filter_is_safe(Node $filterArgs) + { + foreach ($filterArgs as $arg) { + if ($arg instanceof ConstantExpression) { + return [$arg->getAttribute('value')]; + } + + return []; } - return []; + return ['html']; } - - return ['html']; -} } diff --git a/vendor/twig/twig/src/Extension/StringLoaderExtension.php b/vendor/twig/twig/src/Extension/StringLoaderExtension.php index 7b4514710..57f865e7a 100644 --- a/vendor/twig/twig/src/Extension/StringLoaderExtension.php +++ b/vendor/twig/twig/src/Extension/StringLoaderExtension.php @@ -10,33 +10,33 @@ */ namespace Twig\Extension { -use Twig\TwigFunction; + use Twig\TwigFunction; -final class StringLoaderExtension extends AbstractExtension -{ - public function getFunctions(): array + final class StringLoaderExtension extends AbstractExtension { - return [ + public function getFunctions(): array + { + return [ new TwigFunction('template_from_string', 'twig_template_from_string', ['needs_environment' => true]), - ]; + ]; + } } } -} namespace { -use Twig\Environment; -use Twig\TemplateWrapper; + use Twig\Environment; + use Twig\TemplateWrapper; -/** - * Loads a template from a string. - * - * {{ include(template_from_string("Hello {{ name }}")) }} - * - * @param string $template A template as a string or object implementing __toString() - * @param string $name An optional name of the template to be used in error messages - */ -function twig_template_from_string(Environment $env, $template, string $name = null): TemplateWrapper -{ - return $env->createTemplate((string) $template, $name); -} + /** + * Loads a template from a string. + * + * {{ include(template_from_string("Hello {{ name }}")) }} + * + * @param string $template A template as a string or object implementing __toString() + * @param string $name An optional name of the template to be used in error messages + */ + function twig_template_from_string(Environment $env, $template, string $name = null): TemplateWrapper + { + return $env->createTemplate((string) $template, $name); + } } diff --git a/vendor/twig/twig/src/Lexer.php b/vendor/twig/twig/src/Lexer.php index 4fcb223dc..300fe4d59 100644 --- a/vendor/twig/twig/src/Lexer.php +++ b/vendor/twig/twig/src/Lexer.php @@ -250,7 +250,7 @@ private function lexData(): void if (preg_match($this->regexes['lex_block_raw'], $this->code, $match, 0, $this->cursor)) { $this->moveCursor($match[0]); $this->lexRawData(); - // {% line \d+ %} + // {% line \d+ %} } elseif (preg_match($this->regexes['lex_block_line'], $this->code, $match, 0, $this->cursor)) { $this->moveCursor($match[0]); $this->lineno = (int) $match[1]; diff --git a/vendor/twig/twig/src/Markup.php b/vendor/twig/twig/src/Markup.php index c48268ac4..7ae502a4e 100644 --- a/vendor/twig/twig/src/Markup.php +++ b/vendor/twig/twig/src/Markup.php @@ -32,11 +32,13 @@ public function __toString() return $this->content; } + #[\ReturnTypeWillChange] public function count() { return mb_strlen($this->content, $this->charset); } + #[\ReturnTypeWillChange] public function jsonSerialize() { return $this->content; diff --git a/vendor/twig/twig/src/Node/ModuleNode.php b/vendor/twig/twig/src/Node/ModuleNode.php index e972b6ba5..f87083a4b 100644 --- a/vendor/twig/twig/src/Node/ModuleNode.php +++ b/vendor/twig/twig/src/Node/ModuleNode.php @@ -368,9 +368,9 @@ protected function compileGetTemplateName(Compiler $compiler) protected function compileIsTraitable(Compiler $compiler) { // A template can be used as a trait if: - // * it has no parent - // * it has no macros - // * it has no body + // * it has no parent + // * it has no macros + // * it has no body // // Put another way, a template can be used as a trait if it // only contains blocks and use statements. diff --git a/vendor/twig/twig/src/Node/Node.php b/vendor/twig/twig/src/Node/Node.php index d1a5b9001..bc1ed34b4 100644 --- a/vendor/twig/twig/src/Node/Node.php +++ b/vendor/twig/twig/src/Node/Node.php @@ -145,6 +145,7 @@ public function removeNode(string $name): void unset($this->nodes[$name]); } + #[\ReturnTypeWillChange] public function count() { return \count($this->nodes); diff --git a/vendor/twig/twig/src/Sandbox/SecurityPolicy.php b/vendor/twig/twig/src/Sandbox/SecurityPolicy.php index 2fc0d0131..57e937146 100644 --- a/vendor/twig/twig/src/Sandbox/SecurityPolicy.php +++ b/vendor/twig/twig/src/Sandbox/SecurityPolicy.php @@ -50,7 +50,8 @@ public function setAllowedMethods(array $methods): void { $this->allowedMethods = []; foreach ($methods as $class => $m) { - $this->allowedMethods[$class] = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, \is_array($m) ? $m : [$m]); + $this->allowedMethods[$class] = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + }, \is_array($m) ? $m : [$m]); } } diff --git a/vendor/twig/twig/src/Template.php b/vendor/twig/twig/src/Template.php index 14033ced8..3566ef572 100644 --- a/vendor/twig/twig/src/Template.php +++ b/vendor/twig/twig/src/Template.php @@ -213,7 +213,8 @@ public function renderParentBlock($name, array $context, array $blocks = []) if ($this->env->isDebug()) { ob_start(); } else { - ob_start(function () { return ''; }); + ob_start(function () { return ''; + }); } $this->displayParentBlock($name, $context, $blocks); @@ -238,7 +239,8 @@ public function renderBlock($name, array $context, array $blocks = [], $useBlock if ($this->env->isDebug()) { ob_start(); } else { - ob_start(function () { return ''; }); + ob_start(function () { return ''; + }); } $this->displayBlock($name, $context, $blocks, $useBlocks); @@ -373,7 +375,8 @@ public function render(array $context) if ($this->env->isDebug()) { ob_start(); } else { - ob_start(function () { return ''; }); + ob_start(function () { return ''; + }); } try { $this->display($context); diff --git a/vendor/twig/twig/src/TemplateWrapper.php b/vendor/twig/twig/src/TemplateWrapper.php index c9c6b07c6..364cfb61e 100644 --- a/vendor/twig/twig/src/TemplateWrapper.php +++ b/vendor/twig/twig/src/TemplateWrapper.php @@ -67,7 +67,8 @@ public function renderBlock(string $name, array $context = []): string if ($this->env->isDebug()) { ob_start(); } else { - ob_start(function () { return ''; }); + ob_start(function () { return ''; + }); } try { $this->template->displayBlock($name, $context); diff --git a/version.php b/version.php index 9304e4f06..cfed02a65 100644 --- a/version.php +++ b/version.php @@ -22,14 +22,13 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2022110900; +$plugin->version = 2024031500; $plugin->requires = 2022041900; $plugin->cron = 0; $plugin->component = 'qtype_coderunner'; $plugin->maturity = MATURITY_STABLE; -$plugin->release = '5.1.1'; - -$plugin->dependencies = array( - 'qbehaviour_adaptive_adapted_for_coderunner' => 2021112300 -); +$plugin->release = '5.3.1'; +$plugin->dependencies = [ + 'qbehaviour_adaptive_adapted_for_coderunner' => 2021112300, +];