robotframework-xmlvalidator 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1605 @@
1
+ # Copyright 2024-2025 Michael Hallik
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12
+ # implied.
13
+ #
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+
18
+ """
19
+ This module defines the `XmlValidator` class — a Robot Framework test
20
+ library for validating XML files against XSD schemas using the
21
+ `xmlschema` library.
22
+
23
+ The validator supports both individual and batch validation workflows,
24
+ with comprehensive error reporting and optional export to structured
25
+ CSV files.
26
+
27
+ Key Features:
28
+
29
+ - Validation of a single XML file against a specified schema.
30
+ - Batch validation of multiple XML files in a folder, using:
31
+ - A single shared schema.
32
+ - Dynamic schema matching by namespace or file name.
33
+ - Detailed error reporting for validation failures.
34
+ - Namespace-aware validation for strict conformance.
35
+ - Graceful handling of malformed XML or XSD files.
36
+ - Optional export of all collected errors to a CSV file.
37
+
38
+ This module is intended to be imported by Robot Framework test suites
39
+ or executed as a Python module via a direct call.
40
+ """
41
+
42
+
43
+ # pylint: disable=C0103:invalid-name # On account of the module name, that is not snake-cased (required by Robot Framework).
44
+ # pylint: disable=C0302:too-many-lines # On account of the extensive docstrings and annotations.
45
+ # pylint: disable=C0301:line-too-long # On account of tables in docstrings.
46
+
47
+
48
+ # Standard library imports.
49
+ from pathlib import Path
50
+ from typing import Any, Dict, List, Literal, Optional, Tuple
51
+ # Third party library imports.
52
+ from lxml import etree
53
+ from robot.api import Failure, logger
54
+ from robot.api.deco import keyword, library
55
+ from xmlschema import XMLSchema
56
+ # Local application imports.
57
+ from .xml_validator_results import ValidatorResultRecorder, ValidatorResult
58
+ from .xml_validator_utils import ValidatorUtils
59
+
60
+
61
+ @library(scope='GLOBAL')
62
+ class XmlValidator:
63
+ """
64
+ XmlValidator is a `Robot Framework <https://robotframework.org/>`_
65
+ test library for validating XML files against XSD schemas.
66
+
67
+ The library leverages the power of the
68
+ `xmlschema library <https://pypi.org/project/xmlschema/>`_ and is
69
+ designed for both single-file and batch XML validation workflows.
70
+
71
+ It provides structured and detailed reporting of XML parse errors
72
+ (malformed XML content) and XSD violations, schema auto-detection
73
+ and CSV exports of collected errors.
74
+
75
+ Features are described in detail on the `project repo's landing page.
76
+ <https://github.com/MichaelHallik/robotframework-xmlvalidator>`_.
77
+
78
+ **Overview**
79
+
80
+ The main keyword is ``Validate Xml Files``.
81
+
82
+ The other keywords are convenience/helper functions, e.g. ``Reset
83
+ Error Facets``.
84
+
85
+ The ``Validate Xml Files`` validates one or more XML files against one
86
+ or more XSD schema files and collects and reports all encountered
87
+ errors.
88
+
89
+ The type of error that the keyword can detect is not limited to XSD
90
+ violations, but may also pertain to malformed XML files (e.g. parse
91
+ errors), empty files, unmatched XML files (no XSD match found) and
92
+ others.
93
+
94
+ Errors that result from malformed XML files or from XSD violations
95
+ support detailed error reporting. Using the ``error_facets`` argument
96
+ you may specify the details the keyword should collect and report
97
+ about captured errors.
98
+
99
+ When operating in batch mode, the ``Validate Xml Files`` keyword
100
+ always validates the entire set of passed XML files. That is, when
101
+ it encounters an error in a file, it does not simply fail. Rather,
102
+ it collects the error details (as determined by the error_facets
103
+ arg) and then continues validating the current file as well as any
104
+ subsequent file(s).
105
+
106
+ In that fashion the keyword works through the entire set of files.
107
+
108
+ When having finished checking the last file, it will log a summary
109
+ of the test run and then proceed to report all collected errors in
110
+ the console, in the RF log and, optionally, in the form of a CSV
111
+ file.
112
+
113
+ **Batch mode & dynamic XSD matching**
114
+
115
+ The ``Validate Xml Files`` keyword supports validating a single,
116
+ individual XML file against an XSD schema. It also supports batch
117
+ flows, by being able to validate multiple XML files in a specified
118
+ folder against one or more XSD schema files. These XSD files may
119
+ either reside in the same folder (as the XML files) or in a
120
+ different folder.
121
+
122
+ In the latter case (i.e. when multiple schema files are
123
+ involved) XML files are matched dynamically to XSD files, supporting
124
+ either a 'by filename' strategy or a 'by namespace strategy'.
125
+
126
+ That means you can simply pass the paths to a folder containing
127
+ XML files and to a folder containing XSD files and the library
128
+ will determine which XSD schema file to use for each XML file.
129
+
130
+ If the XML and XSD files reside in the same folder, you only
131
+ have to pass one folder path and the library will dynamically pair
132
+ each XML file with the relevant XSD schema file.
133
+
134
+ When no matching XSD schema could be identified for an XML file,
135
+ this will be integrated into the error reporting (the keyword will
136
+ not fail).
137
+
138
+ As mentioned earlier, you may also refer to specific XML/XSD files
139
+ (instead of to folders). In that case, no matching will be
140
+ attempted, but the library will simply try to validate the specified
141
+ XML file against the specified XSD file.
142
+
143
+ **Schema pre-loading**
144
+
145
+ The library supports loading a schema by specifying an ``xsd_path``
146
+ when importing the library in a Robot Framework test suite. The
147
+ preloaded schema is then reused for all validations until
148
+ overridden at the test case level.
149
+
150
+ **Comprehensive, robust and flexible error reporting**
151
+
152
+ - Captures XSD schema violations.
153
+
154
+ - Missing required elements.
155
+ - Cardinality constraints.
156
+ - Datatype mismatches (e.g., invalid `xs:dateTime`).
157
+ - Pattern and enumeration violations.
158
+ - Namespace errors.
159
+ - Etc.
160
+
161
+ - Captures malformed XML.
162
+ - Handles edge cases like empty files or XML files that could not be
163
+ matched to an XSD schema file.
164
+ - Does not fail on errors, but collects encountered errors in all
165
+ files and reports them in a structured format in the console and
166
+ RF log.
167
+ - Supports specifying the details that should be collected for
168
+ encountered errors.
169
+ - Optionally exports the error report to a CSV file, providing the
170
+ file name next to all error details for traceability.
171
+
172
+ **Customizing error collection**
173
+
174
+ Use the ``error_facets`` argument to control which attributes of
175
+ detected errors will be collected and reported. E.g. the element
176
+ locator (XPath), error message, involved namespace and/or the XSD
177
+ validator that failed.
178
+
179
+ Error facets can be set by passing a list of one or more error
180
+ facets, either with the library import and/or on the test case
181
+ level (i.e. when calling the ``Validate XML Files`` keyword).
182
+
183
+ These are the facets (or attributes) that can be collected and
184
+ reported for each encountered error:
185
+
186
+ +---------------+----------------------------------------------------------------------------+
187
+ | Facet | Description |
188
+ +===============+============================================================================+
189
+ | ``message`` | A human-readable message describing the validation error. |
190
+ +---------------+----------------------------------------------------------------------------+
191
+ | ``path`` | The XPath location of the error in the XML document. |
192
+ +---------------+----------------------------------------------------------------------------+
193
+ | ``domain`` | The domain of the error (e.g., "validation"). |
194
+ +---------------+----------------------------------------------------------------------------+
195
+ | ``reason`` | The reason for the error, often linked to XSD constraint violations. |
196
+ +---------------+----------------------------------------------------------------------------+
197
+ | ``validator`` | The XSD component (e.g., element, attribute, type) that failed validation. |
198
+ +---------------+----------------------------------------------------------------------------+
199
+ | ``schema_path`` | The XPath location of the error in the XSD schema. |
200
+ +---------------+----------------------------------------------------------------------------+
201
+ | ``namespaces`` | The namespaces involved in the error (if applicable). |
202
+ +---------------+----------------------------------------------------------------------------+
203
+ | ``elem`` | The XML element that caused the error (``ElementTree.Element``). |
204
+ +---------------+----------------------------------------------------------------------------+
205
+ | ``value`` | The invalid value that triggered the error. |
206
+ +---------------+----------------------------------------------------------------------------+
207
+ | ``severity`` | The severity level of the error (not always present). |
208
+ +---------------+----------------------------------------------------------------------------+
209
+ | ``args`` | The arguments passed to the error message formatting. |
210
+ +---------------+----------------------------------------------------------------------------+
211
+
212
+ For each error that is encountered, the selected error facet(s) will
213
+ be collected and reported.
214
+
215
+ Error facets passed during library initialization will be overruled
216
+ by error facets that are passed at the test case level, when calling
217
+ the ``Validate Xml Files`` keyword.
218
+
219
+ The values you can pass through the `error_facets` argument are
220
+ based on the attributes of the error objects as returned by the
221
+ XMLSchema.iter_errors() method, that is provided by the xmlschema
222
+ library and the the xmlvalidator library leverages. Said method
223
+ yields instances of
224
+ xmlschema.validators.exceptions.XMLSchemaValidationError (or its
225
+ subclasses), each representing a specific validation issue
226
+ encountered in an XML file. These error objects expose various
227
+ attributes that describe the nature, location, and cause of the
228
+ problem.
229
+
230
+ The table lists the most commonly available attributes, though
231
+ additional fields may be available depending on the type of
232
+ validation error.
233
+
234
+ **Support for XSD includes/imports**
235
+
236
+ Enables resolution of schema imports/includes via a custom base URL,
237
+ via the ``base_url`` arg.
238
+
239
+ Use ``base_url`` when your XSD uses <xs:include> or <xs:import> with
240
+ relative paths.
241
+
242
+ You can pass ``base_url`` with the library import (together with
243
+ passing ``xsd_path``) and/or when calling ``Validate Xml Files``
244
+ with ``xsd_path``.
245
+
246
+ ** Optional test case fail **
247
+
248
+ The ``Validate Xml Files`` keyword collects one or more errors for
249
+ one or more XML files. As mentioned earlier, the keyword is designed
250
+ so as to not fail upon encountering errors.
251
+
252
+ However, in case you want your test case to fail when one or more
253
+ errors have been detected, you can use the ``fail_on_errors`` (bool)
254
+ argument to make it so. It defaults to ${False}.
255
+
256
+ **Basic usage examples**
257
+
258
+ For a comprehensive set of example test cases, please see the
259
+ `Robot Framework integration tests <https://github.com/MichaelHallik/robotframework-xmlvalidator/tree/main/test/integration/>`_
260
+ in the projects GitHub repository.
261
+
262
+ The repo contains a
263
+ `structured overview of all implemented tests <https://github.com/MichaelHallik/robotframework-xmlvalidator/blob/main/test/_doc/integration/overview.html>`_
264
+ per topic (e.g. library import, schema matching strategies, etc.).
265
+
266
+ It further contains a detailed instruction on
267
+ `how to run Robot Framework tests <https://github.com/MichaelHallik/robotframework-xmlvalidator/blob/main/test/_doc/integration/README.md>`_.
268
+
269
+ .. code:: robotframework
270
+
271
+ *** Settings ***
272
+ Library XmlValidator xsd_path=path/to/default/schema.xsd
273
+
274
+ *** Variables ***
275
+ ${SINGLE_XML_FILE} path/to/file1.xml
276
+ ${FOLDER_MULTIPLE_XML} path/to/xml_folder_1
277
+ ${FOLDER_MULTIPLE_XML_ALT} path/to/xml_folder_2
278
+ ${FOLDER_MULTIPLE_XML_NS} path/to/xml_folder_3
279
+ ${FOLDER_MULTIPLE_XML_FN} path/to/xml_folder_4
280
+
281
+ ${SINGLE_XSD_FILE} path/to/alt_schema.xsd
282
+ ${FOLDER_MULTIPLE_XSD} path/to/xsd_schemas/
283
+
284
+ *** Test Cases ***
285
+
286
+ Validate Single XML File With Default Schema
287
+ [Documentation] Validates a single XML file using the default schema
288
+ Validate Xml Files ${SINGLE_XML_FILE}
289
+
290
+ Validate Folder Of XML Files With Default Schema
291
+ [Documentation] Validates all XML files in a folder using the default schema
292
+ Validate Xml Files ${FOLDER_MULTIPLE_XML}
293
+
294
+ Validate Folder With Explicit Schema Override
295
+ [Documentation] Validates XML files using a different, explicitly provided schema
296
+ Validate Xml Files ${FOLDER_MULTIPLE_XML_ALT} ${SINGLE_XSD_FILE}
297
+
298
+ Validate Folder With Multiple Schemas By Namespace
299
+ [Documentation] Resolves matching schema for each XML file based on namespace
300
+ Validate Xml Files ${FOLDER_MULTIPLE_XML_NS} ${FOLDER_MULTIPLE_XSD} xsd_search_strategy=by_namespace
301
+
302
+ Validate Folder With Multiple Schemas By File Name
303
+ [Documentation] Resolves schema based on matching file name patterns (no schema path passed)
304
+ Validate Xml Files ${FOLDER_MULTIPLE_XML_FN} xsd_search_strategy=by_file_name
305
+
306
+ Example of the console output where some files passed validation and
307
+ multiple errors have been found for multiple other files:
308
+
309
+ .. code:: console
310
+
311
+ Schema 'schema.xsd' set.
312
+ Collecting error facets: ['path', 'reason'].
313
+ XML Validator ready for use!
314
+ ==============================================================================
315
+ 01 Advanced Validation:: Demo XML validation
316
+ Mapping XML files to schemata by namespace.
317
+ Validating 'valid_1.xml'.
318
+ XML is valid!
319
+ Validating 'valid_2.xml'.
320
+ XML is valid!
321
+ Validating 'valid_3.xml'.
322
+ XML is valid!
323
+ Validating 'xsd_violations_1.xml'.
324
+ Setting new schema file: C:\\Projects\\robotframework-xmlvalidator\\test\\_data\\integration\\TC_01\\schema1.xsd.
325
+ [ WARN ] XML is invalid:
326
+ [ WARN ] Error #0:
327
+ [ WARN ] path: /Employee
328
+ [ WARN ] reason: Unexpected child with tag '{http://example.com/schema1}FullName' at position 2. Tag '{http://example.com/schema1}Name' expected.
329
+ [ WARN ] Error #1:
330
+ [ WARN ] path: /Employee/Age
331
+ [ WARN ] reason: invalid literal for int() with base 10: 'Twenty Five'
332
+ [ WARN ] Error #2:
333
+ [ WARN ] path: /Employee/ID
334
+ [ WARN ] reason: invalid literal for int() with base 10: 'ABC'
335
+ Validating 'valid_.xml_4'.
336
+ XML is valid!
337
+ Validating 'valid_.xml_5'.
338
+ XML is valid!
339
+ Validating 'malformed_xml_1.xml'.
340
+ [ WARN ] XML is invalid:
341
+ [ WARN ] Error #0:
342
+ [ WARN ] reason: Premature end of data in tag Name line 1, line 1, column 37 (file:/C:/Projects/robotframework-xmlvalidator/test/_data/integration/TC_01/malformed_xml_1.xml, line 1)
343
+ [ WARN ] Error #1:
344
+ [ WARN ] reason: Opening and ending tag mismatch: ProductID line 1 and Product, line 1, column 31 (file:/C:/Projects/robotframework-xmlvalidator/test/_data/integration/TC_01/malformed_xml_1.xml, line 1)
345
+ Validating 'xsd_violations_2.xml'.
346
+ Setting new schema file: C:\\Projects\\robotframework-xmlvalidator\\test\\_data\\integration\\TC_01\\schema2.xsd.
347
+ [ WARN ] XML is invalid:
348
+ [ WARN ] Error #0:
349
+ [ WARN ] path: /Product/Price
350
+ [ WARN ] reason: invalid value '99.99USD' for xs:decimal
351
+ [ WARN ] Error #1:
352
+ [ WARN ] path: /Product
353
+ [ WARN ] reason: The content of element '{http://example.com/schema2}Product' is not complete. Tag '{http://example.com/schema2}Price' expected.
354
+ Validating 'valid_.xml_6'.
355
+ XML is valid!
356
+ Validating 'no_xsd_match_1.xml'.
357
+ [ WARN ] XML is invalid:
358
+ [ WARN ] Error #0:
359
+ [ WARN ] reason: No matching XSD found for: no_xsd_match_1.xml.
360
+ Validating 'no_xsd_match_2.xml'.
361
+ [ WARN ] XML is invalid:
362
+ [ WARN ] Error #0:
363
+ [ WARN ] reason: No matching XSD found for: no_xsd_match_2.xml.
364
+ Validation errors exported to 'C:\\test\\01_Advanced_Validation\\errors_2025-03-29_13-54-46-552150.csv'.
365
+ Total_files validated: 11.
366
+ Valid files: 6.
367
+ Invalid files: 5
368
+ 01 Advanced Validation:: Demo XML validation | PASS |
369
+ 21 errors have been detected.
370
+ ========================================================
371
+ 01 Advanced Validation:: Demo XML validation | PASS |
372
+ 1 test, 1 passed, 0 failed
373
+
374
+ In case ``fail_on_errors`` is True, the console output will look like this:
375
+
376
+ .. code:: console
377
+
378
+ Validation errors exported to 'C:\\test\\01_Advanced_Validation\\errors_2025-03-29_13-54-46-552150.csv'.
379
+ Total_files validated: 11.
380
+ Valid files: 6.
381
+ Invalid files: 5.
382
+ 01 Advanced Validation:: Demo XML validation | FAIL |
383
+ 21 errors have been detected.
384
+ ========================================================
385
+ 01 Advanced Validation:: Demo XML validation | FAIL |
386
+ 1 test, 0 passed, 1 failed
387
+
388
+ The corresponding CSV output would in both cases look like:
389
+
390
+ .. code:: text
391
+
392
+ file_name,path,reason
393
+ xsd_violations_1.xml,/Employee/ID,invalid literal for int() with base 10: 'ABC'
394
+ xsd_violations_1.xml,/Employee/Age,invalid literal for int() with base 10: 'Twenty Five'
395
+ xsd_violations_1.xml,/Employee,Unexpected child with tag '{http://example.com/schema1}FullName' at position 2. Tag '{http://example.com/schema1}Name' expected.
396
+ malformed_xml_1.xml,,"Premature end of data in tag Name line 1, line 1, column 37 (file:/C:/Projects/robotframework-xmlvalidator/test/_data/integration/TC_01/schema1_malformed_2.xml, line 1)"
397
+ malformed_xml_1.xml,,"Opening and ending tag mismatch: ProductID line 1 and Product, line 1, column 31 (file:/C:/Projects/robotframework-xmlvalidator/test/_data/integration/TC_01/schema2_malformed_3.xml, line 1)"
398
+ schema2_invalid_1.xml,/Product/Price,invalid value '99.99USD' for xs:decimal
399
+ schema2_invalid_2.xml,/Product,The content of element '{http://example.com/schema2}Product' is not complete. Tag '{http://example.com/schema2}Price' expected.
400
+ no_xsd_match_1.xml,,No matching XSD found for: no_xsd_match_1.xml.
401
+ no_xsd_match_2.xml,,No matching XSD found for: no_xsd_match_2.xml.
402
+
403
+ """
404
+
405
+ __version__ = '1.0.0'
406
+ ROBOT_LIBRARY_DOC_FORMAT = 'reST'
407
+ nr_instances = 0
408
+
409
+ def __init__(
410
+ self,
411
+ xsd_path: Optional[str | Path] = None,
412
+ base_url: Optional[str] = None,
413
+ error_facets: Optional[ List[str] ] = None
414
+ ) -> None:
415
+ """
416
+ **Library Scope**
417
+
418
+ The XmlValidator library has a ``GLOBAL``
419
+ `library scope <https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#library-scope>`_
420
+
421
+
422
+ **Library Arguments**
423
+
424
+ +--------------+--------------+-----------+---------------------------------------------------------------------------------------------+
425
+ | Argument | Type | Required | Description |
426
+ +==============+==============+===========+=============================================================================================+
427
+ | xsd_path | str | No | Path to an XSD file or folder to preload during initialization. |
428
+ | | | | In case of a folder, the folder must hold one file only. |
429
+ +--------------+--------------+-----------+---------------------------------------------------------------------------------------------+
430
+ | base_url | str | No | Base path used to resolve includes/imports within the provided XSD schema. |
431
+ +--------------+--------------+-----------+---------------------------------------------------------------------------------------------+
432
+ | error_facets | list of str | No | The attributes of validation errors to collect and report. E.g. ``path``, ``reason``. |
433
+ +--------------+--------------+-----------+---------------------------------------------------------------------------------------------+
434
+
435
+ All arguments are optional.
436
+
437
+ ``xsd_path``
438
+
439
+ Must be a (valid) path to a single XSD file. If the path
440
+ points to a directory, to a file without '.xsd' extension or
441
+ to an invalid or corrupt XSD file, an appropriate exception
442
+ will be raised.
443
+
444
+ The optional ``xsd_path`` parameter allows pre-loading a
445
+ specific XSD schema file during initialization. This schema
446
+ will then be used as the schema for all subsequent calls to
447
+ ``Validate Xml Files`` that do not themselves pass a path to
448
+ an XSD schema file or to a folder containing one or more XSD
449
+ files.
450
+
451
+ As soon as an ``xsd_path`` (to a schema file or a folder
452
+ holding one or more schema files) is passed to
453
+ ``Validate Xml Files``, at the test case level, a schema
454
+ file that was loaded during library initialization will be
455
+ overriden.
456
+
457
+ If no schema has been set during initialization, then a path
458
+ to an XSD schema file (or a folder holding one or more
459
+ schema files) must be supplied in the very first call to
460
+ ``Validate Xml Files`` to prevent the call from failing.
461
+
462
+ If no schema has been set during library import or by a
463
+ precending call to ``Validate Xml Files``, then any call to
464
+ the keyword that does not provide a schema, will fail.
465
+
466
+ Each time a schema file is passed and set, all subsequent calls
467
+ to ``Validate Xml Files`` will use that schema, unless it is
468
+ replaced by passing a new ``xsd_path`` with a call.
469
+
470
+ ``base_url``
471
+
472
+ The `base_url` parameter is used to resolve relative imports
473
+ and includes within the XSD schema. It should point to the
474
+ base directory or URL for resolving relative paths in the
475
+ XSD schema, such as imports and includes.
476
+
477
+ ``error_facets``
478
+
479
+ This parameter accepts a list of error attributes to collect
480
+ during validation failures. If not provided, it will be set to
481
+ the default: ['path', 'reason'].
482
+
483
+ Using ``error_facets`` you can control which attributes of
484
+ validation errors are to be collected and, ultimately, reported.
485
+
486
+ See the introduction for more details on the purpose and usage
487
+ of error facets.
488
+
489
+ **Examples**
490
+
491
+ Using a preloaded schema:
492
+
493
+ .. code:: robotframework
494
+
495
+ ************* Settings ***
496
+ Library xmlvalidator xsd_path=path/to/schema.xsd
497
+
498
+ Defer schema loading to the test case(s):
499
+
500
+ .. code:: robotframework
501
+
502
+ **********Library xmlvalidator
503
+
504
+ Importing with preloaded XSD that requires a base_url:
505
+
506
+ .. code:: robotframework
507
+
508
+ **********Library xmlvalidator xsd_path=path/to/schema_with_include.xsd
509
+ **********... base_url=path/to/include_schemas
510
+
511
+ Use ``base_url`` when your XSD uses ``<xs:include>`` or ``<xs:import>`` with relative paths.
512
+
513
+ Use the ``error_facets`` argument to control which attributes of detected errors will be collected and reported.
514
+
515
+ E.g. the element locator (XPath), error message, involved namespace and/or the XSD validator that failed.
516
+
517
+ Example:
518
+
519
+ .. code:: robotframework
520
+
521
+ **********Library xmlvalidator error_facets=path, message, validator
522
+
523
+ You can also combine this with a preloaded schema and/or a base_url:
524
+
525
+ .. code:: robotframework
526
+
527
+ **********Library xmlvalidator xsd_path=schemas/schema.xsd
528
+ **********... error_facets=value, namespaces
529
+
530
+ For more examples see the project's
531
+ `Robot Framework integration test suite <https://github.com/MichaelHallik/robotframework-xmlvalidator/blob/main/test/integration/01_library_initialization.robot>`_.
532
+
533
+ **Raises**
534
+
535
+ +---------------+--------------------------------------------------------------+
536
+ | Exception | Description |
537
+ +===============+==============================================================+
538
+ | ValueError | Raised if ``xsd_path`` is provided, but resolves to multiple |
539
+ | | XSD files instead of a single one. |
540
+ +---------------+--------------------------------------------------------------+
541
+ | SystemError | Raised if loading the specified XSD schema fails due to an |
542
+ | | invalid or unreadable file. |
543
+ +---------------+--------------------------------------------------------------+
544
+ | IOError | Raised if the XSD file cannot be accessed due to file |
545
+ | | system restrictions. |
546
+ +---------------+--------------------------------------------------------------+
547
+ """
548
+ # Use composition for flexibility, as helper classes may grow.
549
+ self.validator_utils = ValidatorUtils()
550
+ self.validator_results = ValidatorResultRecorder()
551
+ # Initialize the xsd schema from the xsd_path, if provided.
552
+ if xsd_path:
553
+ # Try to get a single xsd file from the provided xsd_path.
554
+ xsd_file_path, is_single_xsd_file = (
555
+ self.validator_utils.get_file_paths(
556
+ xsd_path, 'xsd'
557
+ )
558
+ )
559
+ # We need (a path to) a single xsd file.
560
+ if not is_single_xsd_file:
561
+ raise ValueError(f"Got multiple xsd files: {xsd_file_path}.")
562
+ # Handle incorrect file extension.
563
+ if xsd_file_path[0].suffix != '.xsd':
564
+ # Raise a load error.
565
+ raise SystemError(
566
+ f"ValueError: {xsd_file_path[0]} is not an XSD file."
567
+ )
568
+ # Try to load the provided XSD file.
569
+ result = self._load_schema(xsd_file_path[0], base_url )
570
+ if result.success:
571
+ # Set the loaded XSD file as default schema.
572
+ self.schema = result.value
573
+ logger.info(
574
+ f"Schema '{self.schema.name}' set.", # type: ignore
575
+ also_console=True)
576
+ else:
577
+ # Or report the load error.
578
+ raise SystemError(f"Loading of schema failed: {result.error}")
579
+ # Or inform the user on what to do.
580
+ else:
581
+ logger.info(
582
+ "No XSD schema set: provide schema(s) during keyword calls.",
583
+ also_console=True
584
+ )
585
+ # And flag the schema attr as None.
586
+ self.schema = None
587
+ # Set the error facets to collect for failed XML validations.
588
+ self.error_facets = error_facets if error_facets else [
589
+ 'path', 'reason'
590
+ ]
591
+ logger.info(
592
+ f"Collecting error facets: {self.error_facets}.",
593
+ also_console=True
594
+ )
595
+ logger.info("XML Validator ready for use!", also_console=True)
596
+ self.nr_instances += 1
597
+ logger.info(f'Number of library instances: {self.nr_instances}.')
598
+
599
+ def _determine_validations(
600
+ self,
601
+ xml_paths: List[Path],
602
+ xsd_path: Optional[str|Path] = None,
603
+ xsd_search_strategy: Optional[
604
+ Literal['by_namespace', 'by_file_name']
605
+ ] = None,
606
+ base_url: Optional[str] = None
607
+ ) -> Dict[Path, Path | None]:
608
+ """
609
+ Constructs a mapping between XML files and the XSD schemas to
610
+ use for their validation.
611
+
612
+ This internal method is used during batch validation workflows
613
+ to determine which XSD schema (if any) should be used for each
614
+ XML file in a given set.
615
+
616
+ Depending on the presence of `xsd_path` and
617
+ `xsd_search_strategy`, it supports the following resolution
618
+ strategies:
619
+
620
+ 1. Single schema (shared across all XML files).
621
+
622
+ If `xsd_path` resolves to a single XSD file, that schema is
623
+ preloaded and reused for all target XML files.
624
+
625
+ 2. Dynamic schema matching (multiple schemas):
626
+
627
+ If `xsd_path` resolves to a directory with multiple `.xsd`
628
+ files, each XML file is matched to a corresponding schema file.
629
+
630
+ There are two ways in which `xsd_path` can resolve to a
631
+ directory:
632
+
633
+ - Either because the passed `xsd_path` is a directory.
634
+ - Or because no `xsd_path` has been passed (hence it is None),
635
+ but a (valid) `xsd_search_strategy` has been passed. In that
636
+ case it is assumed that the passed `xml_paths` is a list
637
+ holding one item that points to a folder holding not only XML
638
+ files, but also their accompanying XSD files. That path will,
639
+ therefore, be assigned to `xsd_path` within the method.
640
+
641
+ XML and XSD files can be matched by either one of these
642
+ strategies:
643
+
644
+ - 'by_namespace' (DEFAULT):
645
+ Matches XSDs based on XML namespace compatibility.
646
+ - 'by_file_name':
647
+ Matches XML and XSD files based on identical stem (base
648
+ name).
649
+
650
+ 3. Fallback to default schema:
651
+
652
+ If neither `xsd_path` nor `xsd_search_strategy` is provided, the
653
+ method assumes a default schema has already been set on the
654
+ class instance (i.e. during library initialization).
655
+
656
+ Args:
657
+
658
+ - xml_paths (List[Path]):
659
+ A list of resolved paths to XML files that need validation.
660
+ - xsd_path (str | Path, optional):
661
+ Either a path to a single `.xsd` file or a directory
662
+ containing multiple `.xsd` files. If not provided, fallback
663
+ behavior applies.
664
+ - xsd_search_strategy (Literal["by_namespace", "by_file_name"], optional):
665
+ Specifies how to dynamically map each XML file to a schema
666
+ when multiple `.xsd` files are present. Required when
667
+ `xsd_path` is a folder.
668
+ - base_url (str, optional):
669
+ An optional base path or URL used when resolving `import` or
670
+ `include` statements within XSD files.
671
+
672
+ Returns:
673
+
674
+ - Dict[Path, Path | None]:
675
+ A dictionary mapping each XML file path to its corresponding
676
+ XSD schema path (or to None if the default schema should be
677
+ used).
678
+
679
+ Raises:
680
+
681
+ - SystemError:
682
+ Raised if schema loading fails during single-schema or dynamic
683
+ resolution. The raised error includes parsing failure details.
684
+
685
+ Notes:
686
+
687
+ - If an XML file cannot be matched to a schema in dynamic
688
+ matching mode, a `FileNotFoundError` is mapped to that file
689
+ path in the return dictionary.
690
+ - If a malformed XMl file is encountered, the resulting parse
691
+ error will be caught and mapped to the path of that file, in the
692
+ return dictionary.
693
+ - This method does not perform validation — only resolution of
694
+ schema-to-XML associations.
695
+ - The returned result is passed to `_validate_xml()` during
696
+ batch validation to apply the resolved schema to each XML.
697
+ """
698
+ # Initialize the return dict.
699
+ validations: dict[Path, Path | None] = {}
700
+ # If no xsd_path is provided, but an xsd_search_strategy IS provided.
701
+ if not xsd_path and xsd_search_strategy:
702
+ # Then assume the XSD file(s) to reside in the XML folder
703
+ xsd_path = xml_paths[0].parent
704
+ # The xsd_path can be either an XSD file or a dir holding the file(s).
705
+ if xsd_path:
706
+ # Determine and resolve the XSD file path(s).
707
+ xsd_paths, is_single_xsd_file = (
708
+ self.validator_utils.get_file_paths(xsd_path, 'xsd')
709
+ )
710
+ # All XML files are to be validated against a single XSD schema.
711
+ if is_single_xsd_file:
712
+ # Load the schema as default schema to use.
713
+ result = self._ensure_schema(xsd_paths[0], base_url)
714
+ # Handle a load error.
715
+ if not result.success:
716
+ raise SystemError(
717
+ f"Loading of schema failed: {result.error}."
718
+ )
719
+ # Ensure downstream schema loading will be skipped.
720
+ xsd_file_paths: List[Path|None] = [None] * len(xml_paths)
721
+ # Create the XML/XSD map.
722
+ validations = dict(
723
+ zip( xml_paths, xsd_file_paths )
724
+ )
725
+ # We got a set of XSDs, that need to be dynamically mapped to XMLs.
726
+ else:
727
+ # Map the XSD files to the XML files.
728
+ validations = self._find_schemas(
729
+ xml_paths,
730
+ xsd_paths,
731
+ xsd_search_strategy if xsd_search_strategy else 'by_namespace',
732
+ base_url
733
+ )
734
+ # No XSD path or search strategy given: assume schema set during init.
735
+ else:
736
+ # Ensure default schema is loaded; raises exception otherwise.
737
+ result = self._ensure_schema(None, None)
738
+ # Ensure downstream schema loading will now be skipped.
739
+ xsd_file_paths: List[Path|None] = [None] * len(xml_paths)
740
+ # Create the XML/XSD map.
741
+ validations = dict(
742
+ zip( xml_paths, xsd_file_paths )
743
+ )
744
+ return validations
745
+
746
+ def _ensure_schema(
747
+ self,
748
+ xsd_path: Optional[Path] = None,
749
+ base_url: Optional[str] = None
750
+ ) -> ValidatorResult:
751
+ """
752
+ Ensures that a schema is available for validation.
753
+
754
+ This internal method guarantees that the `XmlValidator` instance
755
+ has access to an XSD schema.
756
+
757
+ It checks whether a schema is already loaded (in `self.schema`)
758
+ or whether one should be loaded from the provided `xsd_path`.
759
+
760
+ If no `xsd_path` is given but a schema is already loaded (in
761
+ self.schema), that existing schema is simply reused.
762
+
763
+ If a new path is passed, it is always loaded via
764
+ `_load_schema()`, replacing an existing schema (if any).
765
+
766
+ If no `xsd_path` is given and no schema was loaded earlier
767
+ (self.schema is None), the method raises a ValueError.
768
+
769
+ Args:
770
+
771
+ - xsd_path (str or Path, optional):
772
+ Path to the XSD schema to load and set as default. Overrides
773
+ any previously loaded schema.
774
+
775
+ - base_url (str, optional):
776
+ Optional base path used for resolving `import` and `include`
777
+ statements in the schema.
778
+
779
+ Returns:
780
+
781
+ - ValidatorResult:
782
+ A result object indicating whether a valid schema is available
783
+ for XML validation.
784
+ - If `success=True`:
785
+ - The `value` field holds the loaded `XMLSchema` object from
786
+ the `xmlschema` library.
787
+ - This schema can be used to validate one or more XML files
788
+ and will typically be assigned to `self.schema`.
789
+ - If `success=False`:
790
+ - The `error` field contains structured information about
791
+ the reason for failure (e.g., schema file not found, parse
792
+ error).
793
+
794
+ Raises:
795
+
796
+ - ValueError:
797
+ - Raised if neither a schema has been preloaded nor an
798
+ `xsd_path` is provided. In such cases, the method cannot
799
+ proceed and explicitly requires a schema to be available.
800
+ - Note: other schema-related failures (e.g., parsing errors,
801
+ I/O issues) are not raised here. Any such failures are
802
+ captured by _load_schema and then returned as part of a
803
+ `ValidatorResult` object.
804
+
805
+ Notes:
806
+
807
+ - This method does not raise exceptions directly; it wraps
808
+ schema loading results using `ValidatorResult`.
809
+ - Used internally in validation and schema resolution workflows.
810
+ - This method relies on `_load_schema` for the actual schema
811
+ loading process, which validates and parses the XSD schema.
812
+ """
813
+ if not (self.schema or xsd_path):
814
+ raise ValueError(
815
+ "No schema: provide an XSD path during keyword call(s)."
816
+ )
817
+ if self.schema and not xsd_path :
818
+ logger.info(
819
+ f'No new schema set: keeping existing schema {self.schema}.'
820
+ )
821
+ return ValidatorResult(success=True, value=self.schema)
822
+ if not self.schema and xsd_path:
823
+ logger.info(f'Setting schema file: {xsd_path}.', also_console=True)
824
+ if self.schema and xsd_path:
825
+ logger.info(
826
+ f'Setting new schema file: {xsd_path}.',
827
+ also_console=True
828
+ )
829
+ return self._load_schema(xsd_path, base_url) # pyright: ignore
830
+
831
+ def _find_schemas(
832
+ self,
833
+ xml_file_paths: List[Path],
834
+ xsd_file_paths: List[Path],
835
+ search_by: Literal[
836
+ 'by_namespace',
837
+ 'by_file_name'
838
+ ] = 'by_namespace',
839
+ base_url: Optional[str] = None
840
+ ) -> Dict[ Path, Path | None ]:
841
+ """
842
+ Finds matching XSD schemas for XML files using the specified
843
+ search strategy.
844
+
845
+ This internal method performs schema-to-XML matching for batch
846
+ validation workflows, returning a dictionary that maps each XML
847
+ file to its corresponding XSD file. If no match is found for a
848
+ given file, that file is mapped to `None`.
849
+
850
+ Two matching strategies are supported:
851
+
852
+ 1. by_namespace (default):
853
+ - Extracts the set of declared namespaces from each XML file.
854
+ - Compares them to the `targetNamespace` declaration (and
855
+ related declarations) of/in each XSD file.
856
+ - Matches the first schema that shares at least one
857
+ namespace.
858
+ 2. by_file_name:
859
+ - Matches based on file name stem (i.e. file name without
860
+ extension), e.g., `invoice.xml` ↔ `invoice.xsd`.
861
+ - Matches the first schema with a matching base name (if
862
+ any).
863
+
864
+ If an XML file cannot be matched to a schema in dynamic matching
865
+ mode, a `FileNotFoundError` is mapped to that file path in the
866
+ return dictionary.
867
+
868
+ If a malformed XMl file is encountered, the resulting parse
869
+ error will be caught and mapped to the path of that file, in the
870
+ return dictionary.
871
+
872
+ Args:
873
+
874
+ - xml_file_paths (List[Path]):
875
+ A list of paths to XML files to be matched with schemas.
876
+ - xsd_file_paths (List[Path]):
877
+ A list of paths to candidate XSD schema files.
878
+ - search_by (Literal["by_namespace", "by_file_name"], optional):
879
+ The strategy to use for matching XML files to schemas.
880
+ Defaults to `"by_namespace"`.
881
+ - base_url (str, optional):
882
+ An optional base URL used to resolve imports and includes
883
+ during namespace-based schema parsing.
884
+
885
+ Returns:
886
+
887
+ - Dict[Path, Path | None | FileNotFoundError]:
888
+ A mapping from each XML file path to:
889
+ - A matching XSD file path if found.
890
+ - None, if no match is applicable.
891
+ - A `FileNotFoundError` object if the schema lookup failed due
892
+ to loading or parsing errors.
893
+
894
+ Notes:
895
+
896
+ - This method does not perform validation — it only establishes
897
+ schema associations, which are later consumed by
898
+ `_validate_xml()`.
899
+ - Errors during XML parsing or namespace extraction are caught
900
+ and logged and the corresponding file is assigned `None`.
901
+ - XSD schema loading failures (e.g., invalid file, parse error)
902
+ result in a `FileNotFoundError` marker.
903
+ """
904
+ # Prepare the return dictionary.
905
+ validations = {}
906
+ # For each XML file, try to find a matching XSD file.
907
+ logger.info(
908
+ f"Mapping XML files to schemata {search_by.replace('_', ' ')}.",
909
+ also_console=True
910
+ )
911
+ for xml_file_path in xml_file_paths:
912
+ logger.info(f"\tSearching schema for: {xml_file_path.stem}.")
913
+ # Initialize the dict entry with None.
914
+ validations[xml_file_path] = None
915
+ # Namespace-based matching.
916
+ if search_by == "by_namespace":
917
+ # Get the XML's namespace(s).
918
+ try:
919
+ # Get the XML root element.
920
+ xml_root = etree.parse( # pylint: disable=I1101:c-extension-no-member
921
+ str(xml_file_path),
922
+ parser=etree.XMLParser() # pylint: disable=I1101:c-extension-no-member
923
+ ).getroot()
924
+ # Get the namespaces from the root element.
925
+ xml_namespaces = ValidatorUtils.extract_xml_namespaces(
926
+ xml_root,
927
+ include_nested=False
928
+ )
929
+ # Catch and handle any exception.
930
+ except Exception as err: # pylint: disable=W0718:broad-exception-caught
931
+ # Inform the user.
932
+ logger.warn('Processing XML file failed.')
933
+ # Collect the error to be mapped to the xml file.
934
+ validations[xml_file_path] = err
935
+ continue
936
+ # Test each XSD file for a matching namespace.
937
+ for xsd_file_path in xsd_file_paths:
938
+ logger.info(f"Testing schema: {xsd_file_path}.")
939
+ # Load the schema.
940
+ result = self._load_schema(
941
+ xsd_file_path, base_url=base_url
942
+ )
943
+ # Log an error, and continue (instead of failing).
944
+ if not result.success:
945
+ logger.warn(
946
+ f"Matching attempt failed due to exception: {result.error}."
947
+ )
948
+ continue
949
+ # Use match_namespace_to_schema for matching logic.
950
+ match = self.validator_utils.match_namespace_to_schema(
951
+ result.value, # type: ignore
952
+ xml_namespaces, # type: ignore
953
+ )
954
+ # If succesful, flag the xsd file as match & end loop.
955
+ if match:
956
+ logger.info(f"\t\t\tMatch found with: {xsd_file_path}.")
957
+ validations[xml_file_path] = xsd_file_path
958
+ break
959
+ # Filename-based matching.
960
+ elif search_by == "by_file_name":
961
+ for xsd_file_path in xsd_file_paths:
962
+ # Log informative.
963
+ logger.info(f"\t\t\tTesting file name: {xsd_file_path}.")
964
+ # Compare the XSD/XML file names.
965
+ if xsd_file_path.stem == xml_file_path.stem:
966
+ logger.info("\t\t\tFound match.")
967
+ # Assign current XSD path in case of a match & break.
968
+ validations[xml_file_path] = xsd_file_path
969
+ break
970
+ # Locating matching schema failed: go to the next xsd file.
971
+ logger.info("\t\t\tNo match: trying next schema file.")
972
+ continue
973
+ # Handle unsupported/incorrect search strategy.
974
+ else:
975
+ raise ValueError(f"Unsupported search strategy: {search_by}.")
976
+ # If no match has been found.
977
+ if not validations[xml_file_path]:
978
+ logger.info(f"No valid XSD found for {xml_file_path}.")
979
+ validations[xml_file_path] = FileNotFoundError(
980
+ f'No matching XSD found for: {xml_file_path.stem}.'
981
+ )
982
+ return validations
983
+
984
+ def _load_schema(
985
+ self,
986
+ xsd_path: Path,
987
+ base_url: Optional[str] = None
988
+ ) -> ValidatorResult:
989
+ """
990
+ This method is responsible for initializing a schema object,
991
+ using the `xmlschema` library.
992
+
993
+ As such, it parses and loads an XSD schema from a given file
994
+ path, with support for resolving relative paths via the
995
+ `base_url` parameter.
996
+
997
+ If the parse/load attempt of a schema file results in any error
998
+ (e.g. a parse error), that error will be caught, wrapped in a
999
+ ValidatorResult object and returned. See for the latter the
1000
+ 'Returns' section below.
1001
+
1002
+ Args:
1003
+
1004
+ - xsd_path (Path):
1005
+ Path to a `.xsd` schema file to load.
1006
+ - base_url (str, optional):
1007
+ An optional base path or URL for resolving `import` and
1008
+ `include` references inside the XSD schema.
1009
+
1010
+ Returns:
1011
+
1012
+ - ValidatorResult:
1013
+ A result object indicating whether schema loading succeeded.
1014
+ - If `success=True`:
1015
+ - The `value` field contains a fully parsed `XMLSchema`
1016
+ object.
1017
+ - If `success=False`:
1018
+ - The `error` field contains structured error information
1019
+ describing what failed and why (e.g., file unreadable, not
1020
+ well-formed, parse error, etc.).
1021
+
1022
+ Notes:
1023
+
1024
+ - This method never raises exceptions directly; all errors are
1025
+ captured and wrapped in a `ValidatorResult`.
1026
+ - Used internally by `_ensure_schema()` and `_find_schemas()` to
1027
+ guarantee schema readiness.
1028
+ """
1029
+ # Load the XSD schema (with base_url, if provided).
1030
+ try:
1031
+ self.schema = XMLSchema( xsd_path, base_url=base_url )
1032
+ return ValidatorResult( success=True, value=self.schema )
1033
+ # Catcherrors to propagate them upstream.
1034
+ except Exception as e: # pylint: disable=W0718:broad-exception-caught
1035
+ return ValidatorResult(
1036
+ success=False, error={"XMLSchemaValidationError": e}
1037
+ )
1038
+
1039
+ def _validate_xml(self, # pylint: disable=R0913:too-many-arguments disable=R0917:too-many-positional-arguments
1040
+ xml_file_path: Path,
1041
+ xsd_file_path: Optional[Path] = None,
1042
+ base_url: Optional[str] = None,
1043
+ error_facets: Optional[ List[str] ] = None,
1044
+ pre_parse: Optional[bool] = True
1045
+ ) -> Tuple[
1046
+ bool, Optional[
1047
+ List[ dict[str, Any] ]
1048
+ ]
1049
+ ]:
1050
+ """
1051
+ Validates an XML file against the currently loaded or provided
1052
+ XSD schema.
1053
+
1054
+ This method performs the core XML validation logic and is
1055
+ invoked by the public `validate_xml_file` method. It checks an
1056
+ XML file for conformance with the structural and datatype rules
1057
+ defined in the XSD schema.
1058
+
1059
+ It focuses exclusively on validation and generating errors based
1060
+ on the specified facets.
1061
+
1062
+ Errors that are collected and returned can be categorized as
1063
+ follows:
1064
+
1065
+ 1. XSD Schema violations.
1066
+
1067
+ The following types of XSD schema violations are detected during
1068
+ validation:
1069
+
1070
+ 1. Detects missing or extra elements that violate cardinality
1071
+ rules, e.g.:
1072
+ - Verifies that all mandatory elements (minOccurs > 0) are
1073
+ present in the XML.
1074
+ - Ensures that optional elements (minOccurs = 0) do not
1075
+ exceed their maximum allowed occurrences (maxOccurs).
1076
+
1077
+ 2. Sequence and Order Violations:
1078
+ - Validates the order of child elements within a parent
1079
+ element if the schema specifies a sequence model
1080
+ (`<xsd:sequence>`).
1081
+ - Detects elements that are out of order or missing in a
1082
+ sequence.
1083
+
1084
+ 3. Datatype Violations:
1085
+ - Ensures that element and attribute values conform to their
1086
+ specified datatypes (e.g., xs:string, xs:integer,
1087
+ xs:dateTime).
1088
+ - Identifies invalid formats, such as incorrect date or time
1089
+ formats for xs:date and xs:dateTime.
1090
+
1091
+ 4. Pattern and Enumeration Violations:
1092
+ - Checks that values conform to patterns defined using
1093
+ `<xsd:pattern>`.
1094
+ - Ensures that values fall within allowed enumerations
1095
+ specified in the schema.
1096
+
1097
+ 5. Attribute Validation:
1098
+ - Verifies that required attributes are present.
1099
+ - Ensures that attribute values adhere to their declared
1100
+ datatypes and constraints.
1101
+
1102
+ 6. Namespace Compliance:
1103
+ - Validates that elements and attributes belong to the
1104
+ correct namespaces as defined in the schema.
1105
+ - Detects namespace mismatches or missing namespace
1106
+ declarations.
1107
+
1108
+ 7. Group Model Violations:
1109
+ - Validates conformance with `<xsd:choice>` and `<xsd:all>`
1110
+ group models, ensuring correct usage of child elements as
1111
+ per the schema.
1112
+
1113
+ 8. Referential Constraints:
1114
+ - Checks for violations in `<xsd:key>`, `<xsd:keyref>`, and
1115
+ `<xsd:unique>` constraints.
1116
+
1117
+ 9. Document Structure and Completeness:
1118
+ - Ensures that the XML document adheres to the hierarchical
1119
+ structure defined by the schema.
1120
+ - Detects incomplete or improperly nested elements.
1121
+
1122
+ 10. General Schema Violations:
1123
+ - Detects schema-level issues, such as invalid imports or
1124
+ includes, during schema compilation if they affect
1125
+ validation.
1126
+
1127
+ 2. Errors following from malformed XML and/or XSD files.
1128
+
1129
+ The method checks whether upstream handling of the involved
1130
+ XML file (e.g. during dynamic schema matching) resulted in an
1131
+ error. For instance a FileNotFound error or a ParseError.
1132
+
1133
+ The method does this by checking whether the coresponding
1134
+ xsd_file_path is an instance of BaseException. In that case,
1135
+ the method returns early, propagating the error to the
1136
+ downstream error collection & reporting.
1137
+
1138
+ The method tself performs a additonal sanity check on the
1139
+ involved XML/XSD files, calling the sanity_check_files()
1140
+ utility method on both. The latter checks each file whether:
1141
+
1142
+ - it exists
1143
+ - is not empty
1144
+ - is of the correct file type
1145
+ - is well-formed (syntactycally correct)
1146
+
1147
+ In case of a failing check, the method returns early,
1148
+ propagating the involved error to downstream error collection
1149
+ & reporting.
1150
+
1151
+ 3. Invalid XSD schema.
1152
+
1153
+ If schema loading fails, the method returns early,
1154
+ propagating the involved error to downstream error collection
1155
+ & reporting.
1156
+
1157
+ Args:
1158
+
1159
+ - xml_path (str or Path):
1160
+ Path to the XML file to validate.
1161
+
1162
+ - xsd_path (str or Path, optional):
1163
+ Path to an alternative XSD schema file to use for this
1164
+ validation. If not provided, the schema loaded during
1165
+ initialization will be used.
1166
+
1167
+ - error_facets (list, optional):
1168
+ Specifies which attributes of the XMLSchemaValidationError
1169
+ should be included in the error output.
1170
+ This provides flexibility in tailoring the level of detail in
1171
+ the error messages.
1172
+
1173
+ Supported Attributes:
1174
+
1175
+ add_note, args, elem, expected, get_elem_as_string,
1176
+ get_obj_as_string, index, invalid_child, invalid_tag,
1177
+ message, msg, namespaces, obj, occurs, origin_url,
1178
+ particle, path, reason, root, schema_url, source,
1179
+ sourceline, stack_trace, validator, with_traceback
1180
+
1181
+ Defaults to ['path', 'reason'].
1182
+
1183
+ Returns:
1184
+
1185
+ - tuple (is_valid, errors):
1186
+ - is_valid (bool):
1187
+ True if the XML file conforms to the schema, False
1188
+ otherwise.
1189
+ - errors (list of dict):
1190
+ A list of detailed error facets/aspects, each represented as
1191
+ dictionary. The dictionary keys depend on the `error_facets`
1192
+ argument.
1193
+ """
1194
+ # Log informative.
1195
+ logger.info(
1196
+ f"Validating '{xml_file_path.stem}'.", also_console=True
1197
+ )
1198
+ # Check upstream XSD matching led to an err pertaining to the XML.
1199
+ if isinstance(xsd_file_path, BaseException):
1200
+ return False, [{
1201
+ facet: str(xsd_file_path) if facet == 'reason' else ''
1202
+ for facet in (error_facets or self.error_facets)
1203
+ }]
1204
+ # Sanity check the target (XML/XSD) files.
1205
+ sanity_check_result = self.validator_utils.sanity_check_files(
1206
+ [file_path for file_path in [
1207
+ xml_file_path, xsd_file_path
1208
+ ] if isinstance(file_path, Path) and file_path],
1209
+ base_url=base_url,
1210
+ parse_files=pre_parse
1211
+ )
1212
+ if not sanity_check_result.success:
1213
+ # Abort validation if one or more sanity checks failed.
1214
+ logger.warn("File(s) failed basic sanity check.")
1215
+ return False, sanity_check_result.error
1216
+ # Ensure a valid schema is loaded.
1217
+ loading_result = self._ensure_schema(
1218
+ xsd_file_path, base_url
1219
+ )
1220
+ if not loading_result.success:
1221
+ # Abort the validation if schema loading failed.
1222
+ logger.warn("Schema loading failed.")
1223
+ return False, loading_result.error
1224
+ # Validate the XML against the XSD schema.
1225
+ errors = [
1226
+ {
1227
+ # Collect the details/facets for each XSD violation.
1228
+ facet: getattr(err, facet, None)
1229
+ # Error facets to collect determined by arg or instance.
1230
+ for facet in (error_facets or self.error_facets)
1231
+ }
1232
+ # Generate an err obj (with err details) per encountered violation.
1233
+ for err in loading_result.value.iter_errors(xml_file_path) # pyright: ignore
1234
+ ]
1235
+ # Determine validity based on the presence of errors.
1236
+ return (True, None) if len(errors) == 0 else (False, errors)
1237
+
1238
+ @keyword
1239
+ def get_error_facets(self) -> List[str]:
1240
+ """
1241
+ .. raw:: html
1242
+
1243
+ <span style="text-decoration: underline; font-size: 15px;">Description</span>
1244
+
1245
+ Returns the currently configured error facets.
1246
+
1247
+ Error facets determine which attributes are extracted from
1248
+ validation errors (instances of XMLSchemaValidationError).
1249
+
1250
+ These attributes control the structure and detail level of the
1251
+ error dictionaries returned during XML validation.
1252
+
1253
+ .. raw:: html
1254
+
1255
+ <span style="text-decoration: underline; font-size: 15px;">Returns</span>
1256
+
1257
+ A list of active error facets, e.g. ["path", "reason"].
1258
+ """
1259
+ return self.error_facets
1260
+
1261
+ @keyword
1262
+ def get_schema(
1263
+ self,
1264
+ return_schema_name: bool = True
1265
+ ) -> Optional[str|XMLSchema]:
1266
+ """
1267
+ .. raw:: html
1268
+
1269
+ <span style="text-decoration: underline; font-size: 15px;">Description</span>
1270
+
1271
+ Returns the currently loaded schema.
1272
+
1273
+ If no schema is loaded, returns None.
1274
+
1275
+ Otherwise, returns either the schema's `name` or the full schema
1276
+ object, depending on the `return_schema_name` flag.
1277
+
1278
+ .. raw:: html
1279
+
1280
+ <span style="text-decoration: underline; font-size: 15px;">Arguments</span>
1281
+
1282
+ return_schema_name:
1283
+
1284
+ - If True (default), returns the schema's name attribute.
1285
+ - If False, returns the actual XMLSchema object.
1286
+
1287
+ .. raw:: html
1288
+
1289
+ <span style="text-decoration: underline; font-size: 15px;">Returns</span>
1290
+
1291
+ The name of the loaded schema, the schema object itself, or None
1292
+ if no schema is available.
1293
+ """
1294
+ if not self.schema:
1295
+ return None
1296
+ if return_schema_name:
1297
+ return getattr(self.schema, "name", None)
1298
+ return self.schema
1299
+
1300
+ @keyword
1301
+ def log_schema(self, log_name: bool = True):
1302
+ """
1303
+ .. raw:: html
1304
+
1305
+ <span style="text-decoration: underline; font-size: 15px;">Description</span>
1306
+
1307
+ Prints schema information to the console and writes it to the
1308
+ Robot Framework log.
1309
+
1310
+ If `log_name` is True, the schema's name is printed (if
1311
+ available); otherwise, the full schema object is logged.
1312
+
1313
+ .. raw:: html
1314
+
1315
+ <span style="text-decoration: underline; font-size: 15px;">Arguments</span>
1316
+
1317
+ log_name:
1318
+
1319
+ - If True (default), log only the schema's name.
1320
+ - If False, log the full XMLSchema object.
1321
+ """
1322
+ if self.schema and log_name:
1323
+ logger.info(
1324
+ f"Schema currently loaded: {self.schema.name}.",
1325
+ also_console=True
1326
+ )
1327
+ logger.info(
1328
+ f"Schema currently loaded: {self.schema}.",
1329
+ also_console=True
1330
+ )
1331
+
1332
+ @keyword
1333
+ def reset_error_facets(self):
1334
+ """
1335
+ Resets the error facets to their default values.
1336
+
1337
+ By default, only the 'path' and 'reason' attributes of
1338
+ validation errors are collected. This method discards any
1339
+ customizations and reverts to those defaults.
1340
+
1341
+ Prints the the change to the console and in the Robot Framework
1342
+ log.
1343
+ """
1344
+ self.error_facets = ['path', 'reason']
1345
+ logger.info(
1346
+ f"Error facets restored to default: {', '.join(self.error_facets)}.",
1347
+ also_console=True
1348
+ )
1349
+
1350
+ @keyword
1351
+ def reset_errors(self):
1352
+ """
1353
+ Clears all previously stored validation results.
1354
+
1355
+ This keyword resets the internal `ValidatorResultRecorder`
1356
+ instance, discarding any errors, warnings, or file status data
1357
+ collected during validation.
1358
+
1359
+ A confirmation message is logged to the Robot Framework log.
1360
+ """
1361
+ self.validator_results.reset()
1362
+ logger.info("Error collector has been reset.", also_console=True)
1363
+
1364
+ @keyword
1365
+ def reset_schema(self):
1366
+ """
1367
+ Unloads the currently loaded schema.
1368
+
1369
+ This keyword clears the cached schema reference by setting it to
1370
+ None. Future validation calls must provide a new schema.
1371
+
1372
+ A message confirming schema reset is logged to the Robot Framework
1373
+ log.
1374
+ """
1375
+ self.schema = None
1376
+ logger.info("Schema attribute reset: no schema loaded.", also_console=True)
1377
+
1378
+ @keyword
1379
+ def validate_xml_files( # pylint: disable=R0913:too-many-arguments disable=R0914:too-many-locals disable=R0917:too-many-positional-arguments
1380
+ self,
1381
+ xml_path: str | Path,
1382
+ xsd_path: Optional[str | Path] = None,
1383
+ xsd_search_strategy: Optional[
1384
+ Literal['by_namespace', 'by_file_name']
1385
+ ] = None,
1386
+ base_url: Optional[str] = None,
1387
+ error_facets: Optional[ List[str] ] = None,
1388
+ pre_parse: Optional[bool] = True,
1389
+ write_to_csv: Optional[bool] = True,
1390
+ timestamped: Optional[bool] = True,
1391
+ reset_errors: bool = True,
1392
+ fail_on_errors: Optional[bool] = False
1393
+ ) -> Tuple[
1394
+ List[ Dict[str, Any] ],
1395
+ str | None
1396
+ ]:
1397
+ """
1398
+ **Introduction**
1399
+
1400
+ Please make sure to have read the ``Introduction`` to the
1401
+ `keyword doc <https://github.com/MichaelHallik/robotframework-xmlvalidator/blob/main/docs/XmlValidator.html>`_.
1402
+
1403
+ That section contains information that is essential to the
1404
+ effective usage of the ``Validate Xml Files`` keyword and that
1405
+ will not be repeated here, to avoid redundancy.
1406
+
1407
+ This keyword supports *single file* validation, *batch* validation
1408
+ and *dynamic schema mapping*.
1409
+
1410
+ It also supports comprehensive and *configurable* error reporting
1411
+ and the *export* of all collected errors to CSV files.
1412
+
1413
+ **Basic use cases**
1414
+
1415
+ +---+----------------------------+------------------------+------------------------+------------------------------------------------------------+
1416
+ | | xsd_path passed with | xml_path points to | xsd_path points to | Keyword call result |
1417
+ +===+============================+========================+========================+============================================================+
1418
+ | 1 | Library Import | Single XML File | Single XSD File | XML file validated against preloaded schema. |
1419
+ +---+----------------------------+------------------------+------------------------+------------------------------------------------------------+
1420
+ | 2 | Library Import | Folder of XMLs | Single XSD File | Each XML in folder validated against preloaded schema. |
1421
+ +---+----------------------------+------------------------+------------------------+------------------------------------------------------------+
1422
+ | 4 | Keyword Call | Single XML File | Single XSD File | XML file validated against the passed schema. |
1423
+ +---+----------------------------+------------------------+------------------------+------------------------------------------------------------+
1424
+ | 5 | Keyword Call | Folder of XMLs | Single XSD File | Each XML in folder validated against the provided schema. |
1425
+ +---+----------------------------+------------------------+------------------------+------------------------------------------------------------+
1426
+ | 5 | Keyword Call | Single XML File | Folder of XSDs | Keyword attempts to find a matching XSD file, either by |
1427
+ | | | | | namespace or by filename. The latter is determined by arg |
1428
+ | | | | | ``xsd_search_strategy``. If a match is found, the XML file |
1429
+ | | | | | is validated against it. If no match is found, this fact |
1430
+ | | | | | is added to the error report. |
1431
+ +---+----------------------------+------------------------+------------------------+------------------------------------------------------------+
1432
+ | 6 | Keyword Call | Folder of XMLs | Folder of XSDs | The keyword attempts to match each XML to one of the |
1433
+ | | | | | schema files in the folder, either by namespace or by |
1434
+ | | | | | filename. The latter is determined by arg |
1435
+ | | | | | ``xsd_search_strategy``. For each XML that has a matching |
1436
+ | | | | | XSD, the XML is validated. Each XML without a matching XSD |
1437
+ | | | | | is added to the error report, with an appropriate 'reason'.|
1438
+ +---+----------------------------+------------------------+------------------------+------------------------------------------------------------+
1439
+
1440
+ **Error collecting and reporting**
1441
+
1442
+ Validation errors fall into three main categories:
1443
+
1444
+ - XSD violations:
1445
+
1446
+ - Captures cardinality issues, datatype mismatches, enumeration
1447
+ violations, pattern mismatches, namespace errors, etc.
1448
+
1449
+ - Malformed XML:
1450
+
1451
+ - Any syntax/parse issues.
1452
+
1453
+ - File-level issues:
1454
+
1455
+ - Detects empty, non-existent, or .
1456
+ - Uses sanity checks to validate syntax and type before XSD validation.
1457
+
1458
+ - Schema issues:
1459
+
1460
+ - Handles cases of invalid, unreadable, or unmatchable schema files.
1461
+
1462
+ All collected errors can optionally be written to a CSV file.
1463
+ Each row includes error details and the associated file name.
1464
+
1465
+ **Arguments**
1466
+
1467
+ ``xml_path``
1468
+
1469
+ Path to an XML file or a directory containing `.xml` files.
1470
+
1471
+ ``xsd_path``
1472
+
1473
+ Path to a single `.xsd` file or a directory containing one or more
1474
+ `.xsd` files. Required for dynamic schema resolution or schema
1475
+ overrides. Defaults to None.
1476
+
1477
+ ``xsd_search_strategy``
1478
+
1479
+ Strategy for dynamic schema resolution when validating against
1480
+ multiple schemas. Required if `xsd_path` is a directory or not
1481
+ passed at all.
1482
+
1483
+ Defaults to 'by_namespace'.
1484
+
1485
+ ``base_url``
1486
+
1487
+ Base directory for resolving schema imports and includes.
1488
+ Defaults to None.
1489
+
1490
+ ``error_facets``
1491
+
1492
+ List of error details/attributes to include in the collecting
1493
+ and reporting of errors.
1494
+
1495
+ Defaults to ['path', 'reason'].
1496
+
1497
+ ``pre_parse``
1498
+
1499
+ If True, performs well-formedness checks on all XML/XSD files
1500
+ before schema validation. Defaults to True.
1501
+
1502
+ ``write_to_csv``
1503
+
1504
+ If True, writes all collected errors to a CSV file in the same
1505
+ folder as the validated XML(s). Defaults to True.
1506
+
1507
+ ``timestamped``
1508
+
1509
+ Appends a timestamp to the CSV filename for uniqueness.
1510
+ Defaults to True.
1511
+
1512
+ ``reset_errors``
1513
+
1514
+ Clears previously stored validation results before this run.
1515
+ Defaults to True.
1516
+
1517
+ ``fail_on_errors``
1518
+
1519
+ Fails a test cases if, after checking the entire batch of one or
1520
+ XML files, one or more errors have been reported. Error
1521
+ reporting and exporting will not change.
1522
+
1523
+ **Returns**
1524
+
1525
+ A tuple, holding:
1526
+
1527
+ - A list of dictionaries:
1528
+ A list of all validation errors found during the run. Each
1529
+ error is a dictionary with items that are based on the
1530
+ `error_facets`.
1531
+ - A string or None:
1532
+ The path to the generated CSV file if there are errors and
1533
+ `write_to_csv=True`; otherwise None.
1534
+
1535
+ **Raises**
1536
+
1537
+ Since the keyword's purpose is to catch and collect various
1538
+ types of errors, for these errors no exceptions will be raised.
1539
+
1540
+ However, in certain situations the keyword may raise certain
1541
+ exceptions. For instance:
1542
+
1543
+ ``FileNotFoundError``
1544
+
1545
+ For instance if the passed ``xml_path`` is non-existing, points
1546
+ to a non-xml file or points to an empty folder.
1547
+
1548
+ ``IOError``
1549
+
1550
+ If writing the CSV file fails due to filesystem restrictions.
1551
+ """
1552
+ # Reset attributes, if requested.
1553
+ if reset_errors:
1554
+ self.validator_results.reset()
1555
+ # Determine and resolve/normalize the XML file path(s).
1556
+ xml_file_paths, is_single_xml_file = (
1557
+ self.validator_utils.get_file_paths(
1558
+ xml_path, 'xml'
1559
+ )
1560
+ )
1561
+ # Pair each XML file with it's proper XSD counterpart.
1562
+ validations = self._determine_validations(
1563
+ xml_file_paths,
1564
+ xsd_path=xsd_path,
1565
+ xsd_search_strategy=xsd_search_strategy,
1566
+ base_url=base_url
1567
+ )
1568
+ # Validate each XML file with the corresponding schema.
1569
+ for xml_file_path, xsd_file_path in validations.items():
1570
+ # The actual validation.
1571
+ is_valid, errors = self._validate_xml(
1572
+ xml_file_path,
1573
+ xsd_file_path=xsd_file_path,
1574
+ base_url=base_url,
1575
+ error_facets=error_facets,
1576
+ pre_parse=pre_parse
1577
+ )
1578
+ # Process the validation results.
1579
+ if is_valid:
1580
+ self.validator_results.add_valid_file(xml_file_path)
1581
+ else:
1582
+ self.validator_results.add_invalid_file(xml_file_path)
1583
+ self.validator_results.add_file_errors(xml_file_path, errors)
1584
+ self.validator_results.log_file_errors(errors) # type: ignore
1585
+ # Write errors to a single CSV file if requested.
1586
+ if write_to_csv and self.validator_results.errors_by_file:
1587
+ csv_path = self.validator_results.write_errors_to_csv(
1588
+ self.validator_results.errors_by_file,
1589
+ xml_file_paths[0].parent
1590
+ if is_single_xml_file else xml_file_paths[0],
1591
+ include_timestamp=timestamped,
1592
+ file_name_column="file_name"
1593
+ )
1594
+ else:
1595
+ csv_path = None
1596
+ # Log a summary of the test run.
1597
+ self.validator_results.log_summary()
1598
+ if fail_on_errors and self.validator_results.errors_by_file:
1599
+ raise Failure(
1600
+ f"{len(self.validator_results.errors_by_file)} errors have been detected."
1601
+ )
1602
+ return (
1603
+ self.validator_results.errors_by_file,
1604
+ csv_path if csv_path else None
1605
+ )