Skip to content

Conversation

KatKatKateryna
Copy link
Collaborator

@KatKatKateryna KatKatKateryna commented Sep 5, 2025

Unit tests added to run PyQt plugin in a headless mode. This required some changes in the main code:

  • imports of qgis modules are made optional, as qgis is not required for pytests
  • commands requiring qgis types are also made optional (e.g. logging with QgsMessageLog)
  • all typing for qgis namespaces are "quoted" in case the code is running from pytests and the type is not imported

Additionally:

  • YML workflow is added to run unit tests on every push to a PR branch
  • small bug in Provider window (correct widgets' values are passed and read)

Screenshots:
unit_tests_yaml_2

Failed tests:
unit_tests_yaml_failed1
unit_tests_yaml_failed2

KatKatKateryna and others added 12 commits September 4, 2025 18:49
* adjust datetime format

* run tests on PR, show failed tests

* add pytests for file open-save

* typo

* separate the workflow

* absolute import

* stop ignoring yml on commit

* import top level repo

* run pytests from root

* run pytests from the workspace

* update imports in test folder

* init file for tests

* mode importlib

* import qgis as optional

* avoiding qgis imports if possible

* fix imports

* optional typing for qgis imports

* same

* make pyqt5 offscreen mode

* run tests in a virtual display

* Install PyQt5 dependencies manually

* switch to qbot instead of QApplication

* only init dialog

* remove typos

* YAML for windows

* runt test

* ignore Qgs imports

* run a bit more

* print URL

* print from start

* show print statements

* print statements flush

* more prints

* check file location

* more prints

* wrap QgsMessageLog to try/except

* print new file path for checking

* write wrong YAML

* Revert "write wrong YAML"

This reverts commit dd7a6e4.

* fix test

* remove datatime validation (for testing)

* Revert "remove datatime validation (for testing)"

This reverts commit 627f488.
@KatKatKateryna KatKatKateryna marked this pull request as ready for review September 5, 2025 13:13
@doublebyte1
Copy link
Contributor

👍🏽

Copy link
Contributor

@doublebyte1 doublebyte1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding the unit tests, and for including them in the GitHub actions! 🥇

Could you also add a section about setting up and running the tests (standalone) in the README file?

I wrote some comments about reading the yaml test files directory from their repositories. Do you think that makes sense, or maybe there is a reason for having them here?

If we copy them here, I think that we may run into the risk that they become outdated, and we won't even know from which version they belong to. At the same time, reading them from the upstream repositories can also lead to failing tests, and it will be hard to pinpoint if it is because of code changes, or schema changes. I think one other option could also be to tag the config files with a commit (or release) hashtag, and in that way we would know exactly to which version they belong. I would ❤️ to hear your thoughts about this.

I think with all the code that you introduced, you are in a position to implement the most challenging test of all: comparing the input yaml and the output yaml 😺

I am aware this may require a refactoring of the code, but from a user's perspective, I think it is super important for the tool to not change anything that was not strictly updated by the user (even when these changes are apparently harmless).

import subprocess

from ..pygeoapi_config_dialog import PygeoapiConfigDialog

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need a third test, to validate that the yaml that we input is the same as the yaml that we output:

Example:

diff cite.config.yml saved_cite.config.yml - The output of this command should be an empty set.

I foresee some challenges, in order to pass that text (but I am confident you can address them 😁 ):

  • The order of the keys may be different in the returned file; nevertheless that should still yield a valid result.
  • Some optional fields with default values are being added; that should not happen - we can delegate the task of adding default values to pygeoapi. For example this:
    hello-world:
    type: process
    processor:
      name: HelloWorld

Is being transformed into this:

    hello-world:
        type: collection
        title: ''
        description: ''
        keywords: []
        links: []
        extents:
            spatial:
                bbox: [-180, -90, 180, 90]
                crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84
        providers: []

Copy link
Collaborator Author

@KatKatKateryna KatKatKateryna Sep 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the issue here. The test is only writing YAML, while the normal user interaction would be to see the Error window and not be able to proceed with Save file option (the validation check is done before opening a window to choose Save file directory - that part is skipped in unit tests). This is solvable. But the mandatory fields would still be added on file opening, so I'll look for a way around to ignore them in diff.

Copy link
Contributor

@doublebyte1 doublebyte1 Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don´t think you should implement validation for this test. Maybe I should have picked an example of a collection type that is supported (not OGC API - Processes) (:

Let's take the lakes collection, at cite.config.yml:

    lakes:
        type: collection
        title: Large Lakes
        description: lakes of the world, public domain
        keywords:
            - lakes
        crs:
            - CRS84
        links:
            - type: text/html
              rel: canonical
              title: information
              href: http://www.naturalearthdata.com/
              hreflang: en-US
        extents:
            spatial:
                bbox: [-180,-90,180,90]
                crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84
            temporal:
                begin: 2011-11-11T00:00:00Z
                end: null  # or empty
        providers:
            - type: tile
              name: MVT-tippecanoe
              data: ../data/tiles/ne_110m_lakes
              options:
                bounds: [[-124.953634,-16.536406],[109.929807,66.969298]]
                zoom:
                    min: 0
                    max: 5
              format:
                    name: pbf
                    mimetype: application/vnd.mapbox-vector-tile

The tests output this collection:

    lakes:
        type: collection
        title: Large Lakes
        description: lakes of the world, public domain
        keywords:
        - lakes
        links:
        -   type: text/html
            rel: canonical
            href: http://www.naturalearthdata.com/
            title: information
            hreflang: en-US
        extents:
            spatial:
                bbox: [-180, -90, 180, 90]
                crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84
            temporal:
                begin: 2011-11-11T00:00:00Z
        providers:
        -   type: tile
            name: MVT-tippecanoe
            data: ../data/tiles/ne_110m_lakes
            options:
                layer: ''
                style: ''
                version: ''
            format:
                name: pbf
                mimetype: application/vnd.mapbox-vector-tile

The provider options had bounds and a zoom dictionary, but now I see a layer, style and version options that did not exist before, with empty values (maybe these come from WMSFacade?) I think this is the sort of thing that could be captured by a simple test that compares the input yaml and the output yaml (without ny validation).

Copy link
Contributor

@doublebyte1 doublebyte1 Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is happening to the OGC API - Processes collection points out a different issue. What happens to collections or providers that are not supported? I think they should be written in the output file without any changes, but some warning should be thrown to the user stating that a non supported key was detected and that we cannot guarantee the validation of that element. Having elements that are not supported breaks our ability to provide a valid pygeoapi config, but I guess there is not much we can do about that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The process collection is a bit different from the others; [the only required fields are 'type' and 'processor.name']. Type in this case is process, rather than collection.(https://github.com/geopython/pygeoapi/blob/5ed31b47773e7fc0b2a5b1a34055c3b2659d4952/pygeoapi/resources/schemas/config/pygeoapi-config-0.x.yml#L591).

A process is a mathematical function, which may not have a spatial extent. These fields do not apply in the case of processes:

    title: 
    description: 
    keywords:
    links:
    extents:
        spatial:
            bbox:
            crs:

If we cannot disable the validation, maybe it is better to updated the validator to look for these fields.

@KatKatKateryna
Copy link
Collaborator Author

Regarding the files to test against, I would keep them deterministic with the similar set of files to test against. With testing against the versioned config files, what would be that we want to test for exactly?

@doublebyte1
Copy link
Contributor

Regarding the files to test against, I would keep them deterministic with the similar set of files to test against. With testing against the versioned config files, what would be that we want to test for exactly?

Hopefully I have answered that question in my other comments. Let me know, if I didn't (:

@KatKatKateryna
Copy link
Collaborator Author

Thanks, all clear now! :)

Copy link
Contributor

@doublebyte1 doublebyte1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@KatKatKateryna Thanks for adding a script to add the tests locally (and a reference to it in the README): now it is much easier to run the unit tests from the cli. I think that with the sample files you added, we have a variety of configurations that allows us to capture edge cases. 💯

  • Is the convention on the sample yaml file name: organisation_repository_commit hash? I think that works pretty well; could you add to the README too? 🙏🏽
  • When I run the tests locally, it correctly generates all the files; however, it does not output if (and which) tests are passed. This is the output: collected 0 items ========================================= no tests ran in 9.61s ========================================== . Not sure if this is intended, but shouldn't it compare the input and output files and flag differences as errors?
  • There seems to be a problem (at least with some) changes that go through the UI, which are not captured by the tests. The example I noted is the server languages, which have a particular order (that matters). In the case of the unit tests, that order does not change, but if we open the file in the UI and then save it, it does.
  • The non supported providers are still problematic. Ideally, we should not touch those, but if we run the tests, the non supported providers have some keys removed and appended which make them invalid (I have pointed some examples in my comments). If we open these non supported providers in the UI, they are set as read-only, but the validation is still applied. Let me know if this approach makes sense: #9

gzip: false
languages:
# First language is the default language
- en-US
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The order of the items in this array is important, because it defines the default language. I just realised that although this is preserved through the unit tests, it is not the case when we load the file and save it through the UI; this must be because the items are assigned to the UI on loading, and then read from the UI, when saving. Unfortunately we cannot capture these transformations through the unit tests

time_field: created
title_field: title

hello-world:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although this provider is read-only, it is validated when saving through the UI. This means that if there is a missing field here (for instance, description) it won't save the file unless we delete this resource.

import subprocess

from ..pygeoapi_config_dialog import PygeoapiConfigDialog

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The process collection is a bit different from the others; [the only required fields are 'type' and 'processor.name']. Type in this case is process, rather than collection.(https://github.com/geopython/pygeoapi/blob/5ed31b47773e7fc0b2a5b1a34055c3b2659d4952/pygeoapi/resources/schemas/config/pygeoapi-config-0.x.yml#L591).

A process is a mathematical function, which may not have a spatial extent. These fields do not apply in the case of processes:

    title: 
    description: 
    keywords:
    links:
    extents:
        spatial:
            bbox:
            crs:

If we cannot disable the validation, maybe it is better to updated the validator to look for these fields.

@dataclass(kw_only=True)
class ResourceTemporalConfig:

# optional
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are missing an optional 'crs' field on the top level of ResourceConfigTemplate (which is different from the one inside extents.spatial):
https://github.com/geopython/pygeoapi/blob/5ed31b47773e7fc0b2a5b1a34055c3b2659d4952/pygeoapi/resources/schemas/config/pygeoapi-config-0.x.yml#L547

We are also missing the optional "links" key (and its sub-keys), which is resulting in it being wiped out during the saving process.

title: Daily Mean of Water Level or Flow
description: The daily mean is the average of all unit values for a given day.
keywords: [Daily, Daily Mean, Water Level, Flow, Discharge]
crs:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not have support for this key, and if it exists it is disappearing during the saving process (see the comment regarding ResourceConfigTemplate).

begin: str | datetime | None = None
end: str | datetime | None = None
begin: datetime | None = None
end: datetime | None = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spatial.extents.crs key is being updated to the default value of'http://www.opengis.net/def/crs/OGC/1.3/CRS84', regardless of its original value.

map:
url: https://tile.openstreetmap.org/{z}/{x}/{y}.png
attribution: '&copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a>'
manager:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This key (and its sub-keys) is disappearing during the saving process of the cite tests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The temporal extent of lakes collection:

        temporal:
            begin: 2011-11-11
            end: null  # or empty (either means open ended)

Is being converted during the tests to:

        temporal: {}

Maybe because the datetime object is not complete with the timestamp?

In this file we are hitting the problem of a provider that is not supported. As an example, the dutch_georef_stations uses the OGR provider. The tests are appending some fields that do not exist in this provider (data:, host: , dbname: user: ') and removing fields that exist for this provider (gdal_ogr_options, source_options, etc).

@KatKatKateryna
Copy link
Collaborator Author

KatKatKateryna commented Sep 30, 2025

@doublebyte1 thank you for the thorough review! I added the third comparison test and will push it now - the tests will fail. This revealed many issues, including the ones you mentioned.

Addressing some of your points:

  • the local tests should output the results in the command line (I attach the screenshot). If extra files were created, then the tests have run and "no tests ran" seems suspicious. In any case, I will also add a generation of a diff files, so they can be opened and checked manually
image
  • we need to change the widget type to preserve languages order
  • I suggest to split the test files into parts: testing only for Server config, Metadata and Logging, and then individually for all types of supported and unsupported resources and providers. Current DIFF test outputs are not very readable, it's hundreds of lines of the changed text. It will be easier to see the issues by category to know where to implement the fix
  • current implementation of temporal extents assumes ISO format, so 'begin: 2011-11-11' will not be recognized as a date and will default to null
  • I will check the missing/overwritten fields and unsupported Resources and Providers - it should become easier to spot once the test YAMLs are split into small files.

Other notes:

  • some YAML files have 'language' Server configuration, and some 'languages'.
  • there might be errors in the original YAML files, I removed one from the tests for this reason:
image
  • sometimes bbox is written as a 1-line list, and sometimes as a multiline

@doublebyte1
Copy link
Contributor

doublebyte1 commented Oct 1, 2025

@KatKatKateryna Thanks for the changes 👍🏽 . Now I am able to run the tests and get output.

tests_output

I think splitting the tests into different smaller files makes sense. Also, the issues we are having with unimplemented providers only affect the resources part, and it may be easier to tackle them that way.

Regarding the language and languages tags, they are both valid. One is for the case where you just want to support one language, and the other for multiple languages.

https://github.com/geopython/pygeoapi/blob/5ed31b47773e7fc0b2a5b1a34055c3b2659d4952/pygeoapi/resources/schemas/config/pygeoapi-config-0.x.yml#L47-L50

In YAML you can have lists as inline (with square brackets) or in multiple lines, so I think we should support both formats.

https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html

@KatKatKateryna KatKatKateryna marked this pull request as draft October 6, 2025 01:04
@KatKatKateryna
Copy link
Collaborator Author

KatKatKateryna commented Oct 7, 2025

The DIFF files are significantly minimized, so there is no need to test each data type and provider type separately 🙏

  1. Unfortunately I cannot reproduce the issue with 'spatial.extents.CRS' jumping to default value - seems to be working fine. Could you please share the file you are opening, or the changes that you are trying to save? At which stage does the value returns to default?
  2. Resource specification does not seem to have 'crs', it is only for resource providers: https://github.com/geopython/pygeoapi/blob/5ed31b47773e7fc0b2a5b1a34055c3b2659d4952/pygeoapi/resources/schemas/config/pygeoapi-config-0.x.yml . I am not sure why the sample YAML files have this field

Last touches to complete:

  • do not force-add non-mandatory Server bools
  • support non-ISO temporal extents
  • add several more non-mandatory fields in different places (e.g. server.ogc_schemas_location, resource.linked-data)

@doublebyte1
Copy link
Contributor

  1. Unfortunately I cannot reproduce the issue with 'spatial.extents.CRS' jumping to default value - seems to be working fine. Could you please share the file you are opening, or the changes that you are trying to save? At which stage does the value returns to default?

I cannot reproduce it either, nevermind that! 😅

  1. Resource specification does not seem to have 'crs', it is only for resource providers: https://github.com/geopython/pygeoapi/blob/5ed31b47773e7fc0b2a5b1a34055c3b2659d4952/pygeoapi/resources/schemas/config/pygeoapi-config-0.x.yml . I am not sure why the sample YAML files have this field

You are right, but I am a bit puzzled that the collection level metadata crs is present at the configuration files for the demo cite instance:

https://github.com/geopython/demo.pygeoapi.io/blob/e10f3f2e85ab91cb7a4196deeb227efa030f3477/services/pygeoapi_cite/pygeoapi/cite.config.yml#L67

Let me dig a bit in the code and come back to you, on this one. 👍🏽

Last touches to complete:

  • do not force-add non-mandatory Server bools
  • support non-ISO temporal extents
  • add several more non-mandatory fields in different places (e.g. server.ogc_schemas_location, resource.linked-data)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants