diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bfe98e96a..b7bbb072b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,8 @@ jobs: poetry run isort . --check poetry run black . --check + # The "testing" job verifies the base SDK functionality across + # all supported Python versions. testing: needs: styling runs-on: ubuntu-latest @@ -62,10 +64,40 @@ jobs: run: poetry run mypy --ignore-missing-imports - name: unit tests run: poetry run pytest tests/unit --cov=./ --cov-report=xml + - name: build tests + run: poetry build + + # We only run "integration" tests on the latest Python version. + # These tests detect if changes to ontologies, libraries, models and BuildingMOTIF + # affect correct operation of notebooks and BACnet scans. Library integration testing + # is a separate job + integration: + needs: styling + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.11'] + steps: + - name: checkout + uses: actions/checkout@v4 + - uses: actions/setup-java@v4 # for topquadrant shacl support + with: + distribution: 'temurin' + java-version: '21' + - name: setup-python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: install-poetry + uses: snok/install-poetry@v1 + with: + version: 1.4.0 + virtualenvs-in-project: false + virtualenvs-path: ~/.virtualenvs + - name: poetry install + run: poetry install --all-extras - name: integration tests run: poetry run pytest tests/integration - - name: library tests - run: poetry run pytest tests/library - name: bacnet tests run: | cd tests/integration/fixtures/bacnet @@ -73,8 +105,37 @@ jobs: docker compose run -d device docker compose run buildingmotif poetry run pytest -m bacnet docker compose down - - name: build tests - run: poetry build + + # We only run "library" tests on the latest Python version. + # These tests detect if changes to ontologies, libraries, models and BuildingMOTIF + # affect correct operation of templates, shapes, and validation + libraries: + needs: styling + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.11'] + steps: + - name: checkout + uses: actions/checkout@v4 + - uses: actions/setup-java@v4 # for topquadrant shacl support + with: + distribution: 'temurin' + java-version: '21' + - name: setup-python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: install-poetry + uses: snok/install-poetry@v1 + with: + version: 1.4.0 + virtualenvs-in-project: false + virtualenvs-path: ~/.virtualenvs + - name: poetry install + run: poetry install --all-extras + - name: library tests + run: poetry run pytest tests/library coverage: needs: testing diff --git a/buildingmotif-app/src/app/handle-error.ts b/buildingmotif-app/src/app/handle-error.ts new file mode 100644 index 000000000..e77845db7 --- /dev/null +++ b/buildingmotif-app/src/app/handle-error.ts @@ -0,0 +1,16 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { throwError } from 'rxjs'; + +export function handleError(error: HttpErrorResponse) { + if (error.status === 0) { + // A client-side or network error occurred. Handle it accordingly. + console.error('An error occurred:', error.error); + } else { + // The backend returned an unsuccessful response code. + // The response body may contain clues as to what went wrong. + console.error( + `Backend returned code ${error.status}, body was: `, error.error); + } + // Return an observable with a user-facing error message. + return throwError(() => new Error(`${error.status}: ${error.error}`)); +} \ No newline at end of file diff --git a/buildingmotif-app/src/app/library/library.service.ts b/buildingmotif-app/src/app/library/library.service.ts index ff89ae22f..07285a4ba 100644 --- a/buildingmotif-app/src/app/library/library.service.ts +++ b/buildingmotif-app/src/app/library/library.service.ts @@ -4,6 +4,10 @@ import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; import { catchError, retry } from 'rxjs/operators'; +import { environment } from '../../environments/environment'; +import { handleError } from '../handle-error'; + +const API_URL = environment.API_URL; export interface Library { name: string; @@ -35,42 +39,26 @@ export class LibraryService { constructor(private http: HttpClient) { } getAllLibraries() { - return this.http.get("http://localhost:5000/libraries") + return this.http.get(API_URL + `/libraries`) .pipe( retry(3), // retry a failed request up to 3 times - catchError(this.handleError) // then handle the error + catchError(handleError) // then handle the error ); } getAllShapes() { - return this.http.get<{[definition_type: string]: Shape[]}>("http://localhost:5000/libraries/shapes") + return this.http.get<{[definition_type: string]: Shape[]}>(API_URL + `/libraries/shapes`) .pipe( retry(3), // retry a failed request up to 3 times - catchError(this.handleError) // then handle the error + catchError(handleError) // then handle the error ); } getLibrarysTemplates(library_id: number) { - return this.http.get(`http://localhost:5000/libraries/${library_id}?expand_templates=True`) + return this.http.get(API_URL + `/libraries/${library_id}?expand_templates=True`) .pipe( retry(3), // retry a failed request up to 3 times - catchError(this.handleError), // then handle the error + catchError(handleError), // then handle the error ); } - - - private handleError(error: HttpErrorResponse) { - if (error.status === 0) { - // A client-side or network error occurred. Handle it accordingly. - console.error('An error occurred:', error.error); - } else { - // The backend returned an unsuccessful response code. - // The response body may contain clues as to what went wrong. - console.error( - `Backend returned code ${error.status}, body was: `, error.error); - } - // Return an observable with a user-facing error message. - return throwError(() => new Error(`${error.status}: ${error.error}`)); - } - } diff --git a/buildingmotif-app/src/app/model-detail/model-detail.service.ts b/buildingmotif-app/src/app/model-detail/model-detail.service.ts index 2925f5987..5e44caefe 100644 --- a/buildingmotif-app/src/app/model-detail/model-detail.service.ts +++ b/buildingmotif-app/src/app/model-detail/model-detail.service.ts @@ -4,6 +4,10 @@ import { HttpErrorResponse, HttpHeaders } from '@angular/common/http'; import { Model } from '../types' import { Observable, throwError } from 'rxjs'; import { catchError, retry } from 'rxjs/operators'; +import { environment } from '../../environments/environment'; +import { handleError } from '../handle-error'; + +const API_URL = environment.API_URL; @Injectable({ providedIn: 'root' @@ -13,36 +17,36 @@ export class ModelDetailService { constructor(private http: HttpClient) { } getModel(id: number) { - return this.http.get(`http://localhost:5000/models/${id}`) + return this.http.get(API_URL + `/models/${id}`) .pipe( retry(3), // retry a failed request up to 3 times - catchError(this.handleError) // then handle the error + catchError(handleError) // then handle the error ); } getModelGraph(id: number) { - return this.http.get(`http://localhost:5000/models/${id}/graph`, {responseType: 'text'}) + return this.http.get(API_URL + `/models/${id}/graph`, {responseType: 'text'}) .pipe( retry(3), // retry a failed request up to 3 times - catchError(this.handleError) // then handle the error + catchError(handleError) // then handle the error ); } getTargetNodes(id: number) { - return this.http.get(`http://localhost:5000/models/${id}/target_nodes`) + return this.http.get(API_URL + `/models/${id}/target_nodes`) .pipe( retry(3), // retry a failed request up to 3 times - catchError(this.handleError) // then handle the error + catchError(handleError) // then handle the error ); } updateModelGraph(id: number, newGraph: string | File, append: boolean = false) { const headers = {'Content-Type': "application/xml"} - return this.http[append? "patch": "put"](`http://localhost:5000/models/${id}/graph`, newGraph, {headers, responseType: 'text'}) + return this.http[append? "patch": "put"](API_URL + `/models/${id}/graph`, newGraph, {headers, responseType: 'text'}) .pipe( retry(3), // retry a failed request up to 3 times - catchError(this.handleError) // then handle the error + catchError(handleError) // then handle the error ); } diff --git a/buildingmotif-app/src/app/model-new/model-new.service.ts b/buildingmotif-app/src/app/model-new/model-new.service.ts index 54cbbf0b7..efceb7d1a 100644 --- a/buildingmotif-app/src/app/model-new/model-new.service.ts +++ b/buildingmotif-app/src/app/model-new/model-new.service.ts @@ -4,6 +4,10 @@ import { HttpErrorResponse, HttpHeaders } from '@angular/common/http'; import { Model } from '../types' import { Observable, throwError } from 'rxjs'; import { catchError, retry } from 'rxjs/operators'; +import { environment } from '../../environments/environment'; +import { handleError } from '../handle-error'; + +const API_URL = environment.API_URL; @Injectable({ providedIn: 'root' @@ -15,10 +19,10 @@ export class ModelNewService { createModel(name: string, description: string): Observable { const headers = {'Content-Type': "application/json"} - return this.http.post(`http://localhost:5000/models`, {name: name, description: description}, {headers, responseType: 'json'}) + return this.http.post(API_URL + `/models`, {name: name, description: description}, {headers, responseType: 'json'}) .pipe( retry(3), // retry a failed request up to 3 times - catchError(this.handleError) // then handle the error + catchError(handleError) // then handle the error ); } diff --git a/buildingmotif-app/src/app/model-validate/model-validate.service.ts b/buildingmotif-app/src/app/model-validate/model-validate.service.ts index 291aed1a7..b35e0d6b7 100644 --- a/buildingmotif-app/src/app/model-validate/model-validate.service.ts +++ b/buildingmotif-app/src/app/model-validate/model-validate.service.ts @@ -4,7 +4,10 @@ import { HttpErrorResponse } from '@angular/common/http'; import { Model } from '../types' import { throwError } from 'rxjs'; import { catchError, retry } from 'rxjs/operators'; +import { environment } from '../../environments/environment'; +import { handleError } from '../handle-error'; +const API_URL = environment.API_URL; @Injectable({ providedIn: 'root' }) @@ -15,13 +18,13 @@ export class ModelValidateService { validateModel(modelId: number, args: number[]) { const headers = {'Content-Type': "application/json"} - return this.http.post(`http://localhost:5000/models/${modelId}/validate`, + return this.http.post(API_URL + `/models/${modelId}/validate`, {"library_ids": args}, {headers, responseType: 'json'} ) .pipe( retry(3), // retry a failed request up to 3 times - catchError(this.handleError) // then handle the error + catchError(handleError) // then handle the error ); } diff --git a/buildingmotif-app/src/app/pointlabel-parser/pointlabel-parser.service.ts b/buildingmotif-app/src/app/pointlabel-parser/pointlabel-parser.service.ts index ac6bd9553..48858632c 100644 --- a/buildingmotif-app/src/app/pointlabel-parser/pointlabel-parser.service.ts +++ b/buildingmotif-app/src/app/pointlabel-parser/pointlabel-parser.service.ts @@ -4,6 +4,10 @@ import { HttpErrorResponse, HttpHeaders } from '@angular/common/http'; import { Model } from '../types' import { Observable, throwError } from 'rxjs'; import { catchError, retry } from 'rxjs/operators'; +import { environment } from '../../environments/environment'; +import { handleError } from '../handle-error'; + +const API_URL = environment.API_URL; @Injectable({ providedIn: 'root' @@ -15,10 +19,10 @@ export class PointlabelParserService { parse(pointlabels: string, parsers: string): Observable { const headers = {'Content-Type': "application/json"} - return this.http.post(`http://localhost:5000/parsers`, {point_labels: pointlabels, parsers}, {headers, responseType: 'json'}) + return this.http.post(API_URL + `/parsers`, {point_labels: pointlabels, parsers}, {headers, responseType: 'json'}) .pipe( retry(3), // retry a failed request up to 3 times - catchError(this.handleError) // then handle the error + catchError(handleError) // then handle the error ); } diff --git a/buildingmotif-app/src/app/shape-validation/shape-validate.service.ts b/buildingmotif-app/src/app/shape-validation/shape-validate.service.ts index 2052d70a7..83414be36 100644 --- a/buildingmotif-app/src/app/shape-validation/shape-validate.service.ts +++ b/buildingmotif-app/src/app/shape-validation/shape-validate.service.ts @@ -4,6 +4,10 @@ import { HttpErrorResponse } from '@angular/common/http'; import { Model } from '../types' import { throwError } from 'rxjs'; import { catchError, retry } from 'rxjs/operators'; +import { environment } from '../../environments/environment'; +import { handleError } from '../handle-error'; + +const API_URL = environment.API_URL; @Injectable({ providedIn: 'root' @@ -15,13 +19,13 @@ export class ShapeValidationService { validateModelShape(modelId: number, shape_collection_ids: number[], shape_uris: string[], target_class: string) { const headers = {'Content-Type': "application/json"} - return this.http.post>(`http://localhost:5000/models/${modelId}/validate_shape`, + return this.http.post>(API_URL + `/models/${modelId}/validate_shape`, {shape_collection_ids, shape_uris, target_class}, {headers, responseType: 'json'} ) .pipe( retry(3), // retry a failed request up to 3 times - catchError(this.handleError) // then handle the error + catchError(handleError) // then handle the error ); } diff --git a/buildingmotif-app/src/app/template-detail/template-detail.service.ts b/buildingmotif-app/src/app/template-detail/template-detail.service.ts index ff1286c83..15d770fad 100644 --- a/buildingmotif-app/src/app/template-detail/template-detail.service.ts +++ b/buildingmotif-app/src/app/template-detail/template-detail.service.ts @@ -4,6 +4,10 @@ import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; import { catchError, retry } from 'rxjs/operators'; +import { environment } from '../../environments/environment'; +import { handleError } from '../handle-error'; + +const API_URL = environment.API_URL; export interface Template { name: string; @@ -20,10 +24,10 @@ export class TemplateDetailService { constructor(private http: HttpClient) { } getTemplate(id: number, includeParameters: boolean =false) { - return this.http.get