Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: default metrics on file change in case default scenario is not a… #2828

Merged
merged 5 commits into from
Jun 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/)

## [unreleased] (Added 🚀 | Changed | Removed 🗑 | Fixed 🐞 | Chore 👨‍💻 👩‍💻)

## Fixed 🐞

- Default selected metrics on file changes when default scenario is not applicable [#2828](https://github.com/MaibornWolff/codecharta/pull/2828)

## [1.97.0] - 2022-05-31

### Added 🚀
Expand Down
30 changes: 0 additions & 30 deletions visualization/app/codeCharta/codeCharta.service.spec.ts
Expand Up @@ -254,36 +254,6 @@ describe("codeChartaService", () => {
expect(storeService.getState().files[1].selectedAs).toEqual("Partial")
})

it("should load the default scenario after loading a valid file", async () => {
await codeChartaService.loadFiles([
{
fileName,
content: validFileContent,
fileSize: 42
}
])

expect(storeService.getState().dynamicSettings.areaMetric).toEqual("rloc")
expect(storeService.getState().dynamicSettings.heightMetric).toEqual("mcc")
expect(storeService.getState().dynamicSettings.colorMetric).toEqual("mcc")
})

it("should not load the default scenario after loading a valid file, that does not have the required metrics", async () => {
metricData.pop()

await codeChartaService.loadFiles([
{
fileName,
content: validFileContent,
fileSize: 42
}
])

expect(storeService.getState().dynamicSettings.areaMetric).toBeNull()
expect(storeService.getState().dynamicSettings.heightMetric).toBeNull()
expect(storeService.getState().dynamicSettings.colorMetric).toBeNull()
})

it("should show error on invalid file", async () => {
const expectedError: CCFileValidationResult[] = [
{
Expand Down
14 changes: 0 additions & 14 deletions visualization/app/codeCharta/codeCharta.service.ts
Expand Up @@ -3,13 +3,10 @@ import { NodeDecorator } from "./util/nodeDecorator"
import { StoreService } from "./state/store.service"
import { setFiles, setMultipleByNames } from "./state/store/files/files.actions"
import { DialogService } from "./ui/dialog/dialog.service"
import { setState } from "./state/store/state.actions"
import { ScenarioHelper } from "./util/scenarioHelper"
import { setIsLoadingFile } from "./state/store/appSettings/isLoadingFile/isLoadingFile.actions"
import { FileSelectionState, FileState } from "./model/files/files"
import { getCCFile } from "./util/fileHelper"
import { setRecentFiles } from "./state/store/dynamicSettings/recentFiles/recentFiles.actions"
import { nodeMetricDataSelector } from "./state/selectors/accumulatedData/metricData/nodeMetricData.selector"
import { NameDataPair } from "./codeCharta.model"

export class CodeChartaService {
Expand Down Expand Up @@ -44,7 +41,6 @@ export class CodeChartaService {
this.storeService.dispatch(setMultipleByNames(this.recentFiles))

CodeChartaService.updateRootData(rootName)
this.setDefaultScenario()

this.fileStates = []
this.recentFiles = []
Expand Down Expand Up @@ -123,16 +119,6 @@ export class CodeChartaService {
return newFileName
}

private setDefaultScenario() {
const { areaMetric, heightMetric, colorMetric } = ScenarioHelper.getDefaultScenarioSetting().dynamicSettings
const names = [areaMetric, heightMetric, colorMetric]
const metricNames = new Set(nodeMetricDataSelector(this.storeService.getState()).map(x => x.name))

if (names.every(metric => metricNames.has(metric))) {
this.storeService.dispatch(setState(ScenarioHelper.getDefaultScenarioSetting()))
}
}

static updateRootData(rootName: string) {
CodeChartaService.ROOT_NAME = rootName
CodeChartaService.ROOT_PATH = `/${CodeChartaService.ROOT_NAME}`
Expand Down
@@ -1,9 +1,15 @@
import { ApplicationInitStatus } from "@angular/core"
import { TestBed } from "@angular/core/testing"
import { Subject } from "rxjs"
import { RecursivePartial, Settings } from "../../../codeCharta.model"
import { ScenarioHelper } from "../../../util/scenarioHelper"
import { EffectsModule } from "../../angular-redux/effects/effects.module"
import { Store } from "../../angular-redux/store"
import { setAreaMetric } from "../../store/dynamicSettings/areaMetric/areaMetric.actions"
import { setColorMetric } from "../../store/dynamicSettings/colorMetric/colorMetric.actions"
import { setDistributionMetric } from "../../store/dynamicSettings/distributionMetric/distributionMetric.actions"
import { setHeightMetric } from "../../store/dynamicSettings/heightMetric/heightMetric.actions"
import { setState } from "../../store/state.actions"
import { ResetChosenMetricsEffect } from "./resetChosenMetrics.effect"

describe("resetChosenMetricsEffect", () => {
Expand Down Expand Up @@ -32,8 +38,32 @@ describe("resetChosenMetricsEffect", () => {
expect(mockedStore.dispatch).not.toHaveBeenCalled()
})

it("should reset chosen distribution metric", () => {
it("should apply default scenario, when area-, height- and color-metric of default scenario are available", () => {
const defaultScenario = {
dynamicSettings: { areaMetric: "rloc", heightMetric: "rloc", colorMetric: "rloc" }
} as RecursivePartial<Settings>
ScenarioHelper.getDefaultScenarioSetting = () => defaultScenario
mockedNodeMetricDataSelector.next([{ name: "rloc", maxValue: 9001 }])

expect(mockedStore.dispatch).toHaveBeenCalledTimes(2)
expect(mockedStore.dispatch).toHaveBeenCalledWith(setDistributionMetric("rloc"))
expect(mockedStore.dispatch).toHaveBeenCalledWith(setState(defaultScenario))
})

it("should not apply default scenario, when area-, height- or color-metric of default scenario are not available but default to first existing metrics", () => {
const defaultScenario = {
dynamicSettings: { areaMetric: "rloc", heightMetric: "loc", colorMetric: "mcc" }
} as RecursivePartial<Settings>
ScenarioHelper.getDefaultScenarioSetting = () => defaultScenario
mockedNodeMetricDataSelector.next([
{ name: "rloc", maxValue: 9001 },
{ name: "loc", maxValue: 9001 }
])

expect(mockedStore.dispatch).toHaveBeenCalledTimes(4)
expect(mockedStore.dispatch).toHaveBeenCalledWith(setDistributionMetric("rloc"))
expect(mockedStore.dispatch).toHaveBeenCalledWith(setAreaMetric("rloc"))
expect(mockedStore.dispatch).toHaveBeenCalledWith(setHeightMetric("loc"))
expect(mockedStore.dispatch).toHaveBeenCalledWith(setColorMetric("loc"))
})
})
@@ -1,11 +1,16 @@
import { Inject, Injectable } from "@angular/core"
import { tap } from "rxjs"
import { isAnyMetricAvailable } from "./utils/metricHelper"
import { filter, tap } from "rxjs"
import { defaultNMetrics, isAnyMetricAvailable, areScenarioSettingsApplicable } from "./utils/metricHelper"
import { createEffect } from "../../angular-redux/effects/createEffect"
import { Store } from "../../angular-redux/store"
import { nodeMetricDataSelector } from "../../selectors/accumulatedData/metricData/nodeMetricData.selector"
import { setState } from "../../store/state.actions"
import { ScenarioHelper } from "../../../util/scenarioHelper"
import { setDistributionMetric } from "../../store/dynamicSettings/distributionMetric/distributionMetric.actions"
import { getDefaultDistribution } from "./utils/getDefaultDistributionMetric"
import { setAreaMetric } from "../../store/dynamicSettings/areaMetric/areaMetric.actions"
import { setHeightMetric } from "../../store/dynamicSettings/heightMetric/heightMetric.actions"
import { setColorMetric } from "../../store/dynamicSettings/colorMetric/colorMetric.actions"

@Injectable()
export class ResetChosenMetricsEffect {
Expand All @@ -14,10 +19,18 @@ export class ResetChosenMetricsEffect {
resetChosenDistributionMetric$ = createEffect(
() =>
this.store.select(nodeMetricDataSelector).pipe(
filter(isAnyMetricAvailable),
tap(nodeMetricData => {
if (isAnyMetricAvailable(nodeMetricData)) {
// when migrating area, height and color service, their resetting will be added here as well
this.store.dispatch(setDistributionMetric(getDefaultDistribution(nodeMetricData)))
this.store.dispatch(setDistributionMetric(getDefaultDistribution(nodeMetricData)))

const defaultScenarioSettings = ScenarioHelper.getDefaultScenarioSetting()
if (areScenarioSettingsApplicable(defaultScenarioSettings, nodeMetricData)) {
this.store.dispatch(setState(defaultScenarioSettings))
} else {
const [defaultedAreaMetric, defaultedHeightMetric, defaultedColorMetric] = defaultNMetrics(nodeMetricData, 3)
this.store.dispatch(setAreaMetric(defaultedAreaMetric))
this.store.dispatch(setHeightMetric(defaultedHeightMetric))
this.store.dispatch(setColorMetric(defaultedColorMetric))
}
})
),
Expand Down
@@ -1,4 +1,4 @@
import { getMetricNameFromIndexOrLast, isAnyMetricAvailable, isMetricUnavailable } from "./metricHelper"
import { defaultNMetrics, isAnyMetricAvailable, areScenarioSettingsApplicable } from "./metricHelper"

describe("metricHelper", () => {
describe("isAnyMetricAvailable", () => {
Expand All @@ -11,31 +11,41 @@ describe("metricHelper", () => {
})
})

describe("isMetricUnavailable", () => {
it("should return true, when given metric is not available", () => {
expect(isMetricUnavailable([{ name: "a", maxValue: 1 }], "b")).toBe(true)
describe("defaultNMetrics", () => {
it("should default first n metrics with value", () => {
const metricData = [
{ name: "a", maxValue: 0 },
{ name: "b", maxValue: 2 }
]
expect(defaultNMetrics(metricData, 1)).toEqual(["b"])
})

it("should return false, when given metric is available", () => {
expect(isMetricUnavailable([{ name: "a", maxValue: 1 }], "a")).toBe(false)
it("should fill up default metrics with last metric which had a value", () => {
const metricData = [
{ name: "a", maxValue: 0 },
{ name: "b", maxValue: 2 },
{ name: "c", maxValue: 0 }
]
expect(defaultNMetrics(metricData, 2)).toEqual(["b", "b"])
})

it("should throw in case there are no metrics available", () => {
const metricData = [{ name: "a", maxValue: 0 }]
expect(() => defaultNMetrics(metricData, 2)).toThrow()
})
})

describe("getMetricNameFromIndexOrLast", () => {
it("should return metric at given index if it exists", () => {
expect(
getMetricNameFromIndexOrLast(
[
{ name: "a", maxValue: 1 },
{ name: "b", maxValue: 2 }
],
0
)
).toBe("a")
describe("areScenarioSettingsApplicable", () => {
it("should return true when area-, height- and colorMetric is available", () => {
const scenario = { dynamicSettings: { areaMetric: "a", heightMetric: "a", colorMetric: "a" } }
const metricData = [{ name: "a", maxValue: 3 }]
expect(areScenarioSettingsApplicable(scenario, metricData)).toBe(true)
})

it("should return metric at last index if given index is to large", () => {
expect(getMetricNameFromIndexOrLast([{ name: "a", maxValue: 1 }], 1)).toBe("a")
it("should return false when there is no colorMetric", () => {
const scenario = { dynamicSettings: { areaMetric: "a", heightMetric: "a", colorMetric: "b" } }
const metricData = [{ name: "a", maxValue: 3 }]
expect(areScenarioSettingsApplicable(scenario, metricData)).toBe(false)
})
})
})
@@ -1,13 +1,36 @@
import { NodeMetricData } from "../../../../codeCharta.model"
import { NodeMetricData, RecursivePartial, Settings } from "../../../../codeCharta.model"

export function isAnyMetricAvailable(metricData: Pick<NodeMetricData, "maxValue">[]) {
export function isAnyMetricAvailable<T extends Pick<NodeMetricData, "maxValue">[]>(metricData: T) {
return metricData.some(x => x.maxValue > 0)
}

export function isMetricUnavailable(metricData: Pick<NodeMetricData, "maxValue" | "name">[], metricName: string) {
return !metricData.some(x => x.maxValue > 0 && x.name === metricName)
export function areScenarioSettingsApplicable(scenario: RecursivePartial<Settings>, nodeMetricData: Pick<NodeMetricData, "name">[]) {
const { areaMetric, heightMetric, colorMetric } = scenario.dynamicSettings
const relevantMetrics = [areaMetric, heightMetric, colorMetric]
const existingMetrics = new Set(nodeMetricData.map(x => x.name))
return relevantMetrics.every(relevantMetric => existingMetrics.has(relevantMetric))
}

export function getMetricNameFromIndexOrLast<T extends Pick<NodeMetricData, "maxValue" | "name">>(metricData: T[], index: number) {
return metricData[index < metricData.length ? index : metricData.length - 1].name
export function defaultNMetrics<T extends Pick<NodeMetricData, "maxValue" | "name">>(metricData: T[], n: number) {
const defaultedMetrics: string[] = []
let lastMetricNameWithValue: string
for (const metric of metricData) {
if (!metric.maxValue) {
continue
}
defaultedMetrics.push(metric.name)
lastMetricNameWithValue = metric.name
if (--n === 0) {
return defaultedMetrics
}
}

if (!lastMetricNameWithValue) {
throw new Error("there are no metrics available")
}

while (n-- > 0) {
defaultedMetrics.push(lastMetricNameWithValue)
}
return defaultedMetrics
}