diff --git a/DocsExamples/Example-rules.json b/DocsExamples/Example-rules.json index 4385872..618138c 100644 --- a/DocsExamples/Example-rules.json +++ b/DocsExamples/Example-rules.json @@ -8,6 +8,7 @@ "codepage": 1200, "rules": [ { + "id": "CHARTS_WIDER_THAN_TALL", "name": "Charts wider than tall", "description": "Want to check that your charts are wider than tall?", "disabled": false, @@ -49,6 +50,7 @@ ] }, { + "id": "DISABLE_SLOW_DATASOURCE_SETTINGS", "name": "Disable local slow datasource settings", "description": "Check that report slow data source settings are all disabled.", "disabled": false, @@ -91,6 +93,7 @@ ] }, { + "id": "LOCAL_REPORT_SETTINGS", "name": "Local report settings", "disabled": false, "logType": "warning", @@ -213,7 +216,8 @@ ] }, { - "name": "Show visual axes title", + "id": "SHOW_AXES_TITLES", + "name": "Show visual axes titles", "description": "Check that certain charts have both axes title showing.", "disabled": false, "logType": "warning", @@ -282,6 +286,7 @@ ] }, { + "id": "PERCENTAGE_OF_CHARTS_USING_CUSTOM_COLOURS", "name": "Percentage of charts across the report using custom colours is not greater than 10%", "description": "Check that charts avoid custom colours and use theme colours instead.", "disabled": false, @@ -372,6 +377,7 @@ ] }, { + "id": "ENSURE_ALT_TEXT_DEFINED_FOR_VISUALS", "name": "Ensure alt-text has been defined for visuals", "description": "Alt-text is required for screen readers.", "disabled": true, @@ -441,6 +447,7 @@ ] }, { + "id": "DISABLE_DROP_SHADOWS_ON_VISUALS", "name": "Disable drop shadows on visuals", "description": "Drop shadows are not suitable for everyone, this rule returns an array of visuals with drop shadows enabled.", "disabled": false, @@ -485,6 +492,7 @@ ] }, { + "id": "GIVE_VISIBLE_PAGES_MEANINGFUL_NAMES", "name": "Give visible pages meaningful names", "description": "Returns an array of visible page names with a default 'Page x' display name.", "disabled": false, @@ -533,6 +541,7 @@ ] }, { + "id": "DENEB_CHARTS_PROPERTIES", "name": "Check Deneb charts properties - work in progress", "description": "Checks that the drillvar custom rule can read deneb custom visual nested jsonspec properties. This is an example in progress that demonstrates the use of the drillvar custom rule but doesn't yet do anything useful.", "disabled": true, @@ -581,6 +590,7 @@ ] }, { + "id": "CHECK_FOR_VISUALS_OVERLAP", "name": "Check for visuals overlap with a 5px margin", "description": "Returns names of visuals that overlap while inflating visuals rectangle area by 5px left, right, top and bottom. Currently this does not check for overlap with the sides of report page itself. This rule does not currently work with visual groups.", "disabled": false, @@ -691,10 +701,11 @@ ] }, { + "id": "CHECK_FOR_LOCAL_MEASURES", "name": "Check for locally defined measures", "description": "Returns an array of report-level measure definitions", "path": "$.config", - "pathErrorWhenNoMatch": true, + "pathErrorWhenNoMatch": false, "test": [ { "filter": [ @@ -751,6 +762,7 @@ "codepage": 65001, "rules": [ { + "id": "REPORT_THEME_NAME", "name": "Report theme name", "description": "Check Report theme name", "disabled": false, @@ -771,6 +783,7 @@ ] }, { + "id": "REPORT_THEME_TITLE_FONT", "name": "Report theme title font properties", "description": "Checks theme's' title foreground, fontface and fontsize", "disabled": false, diff --git a/DocsExamples/Themeable-properties-rules.json b/DocsExamples/Themeable-properties-rules.json new file mode 100644 index 0000000..58a4a67 --- /dev/null +++ b/DocsExamples/Themeable-properties-rules.json @@ -0,0 +1,1038 @@ +{ + "pbiEntries": [ + { + "name": "reportLayout", + "pbixEntryPath": "Report/Layout", + "pbipEntryPath": "report.json", + "contentType": "json", + "codepage": 1200, + "rules": [ + { + "id": "GENERAL_PROPERTIES_PRESENT", + "name": "Consider using a theme file to define general properties.", + "description": "", + "disabled": false, + "logType": "warning", + "forEachPath": "$.sections[*]", + "forEachPathName": "$.name", + "forEachPathDisplayName": "$.displayName", + "path": "$.visualContainers[*].config", + "pathErrorWhenNoMatch": false, + "test": [ + { + "map": [ + { + "filter": [ + { + "var": "visualsConfigArray" + }, + { + "and": [ + { + "!": [ + { + "in": [ + { + "var": "singleVisual.visualType" + }, + [ + ] + ] + } + ] + }, + { + ">": [ + { + "count": [ + { + "filter": [ + { + "var": "singleVisual.vcObjects.general" + }, + { "!": { "var": "properties.altText" } } + ] + } + ] + }, + 0 + ] + } + ] + } + ] + }, + { + "torecord": [ + "name", + { + "var": "name" + }, + "generalProperties", + { + "filter": [ + { + "var": "singleVisual.vcObjects.general" + }, + { "!": { "var": "properties.altText" } } + ] + } + ] + } + ] + }, + { + "visualsConfigArray": "." + }, + [] + ] + }, + { + "id": "BORDER_PROPERTIES_PRESENT", + "name": "Consider using a theme file to define border properties.", + "description": "", + "disabled": false, + "logType": "warning", + "forEachPath": "$.sections[*]", + "forEachPathName": "$.name", + "forEachPathDisplayName": "$.displayName", + "path": "$.visualContainers[*].config", + "pathErrorWhenNoMatch": false, + "test": [ + { + "map": [ + { + "filter": [ + { + "var": "visualsConfigArray" + }, + { + "and": [ + { + "!": [ + { + "in": [ + { + "var": "singleVisual.visualType" + }, + [ + "specify comma separated list of excluded visual types here" + ] + ] + } + ] + }, + { + "!": [ + { + "in": [ + { + "var": "name" + }, + [ + "specify comma separated list of excluded visual names here" + ] + ] + } + ] + }, + { + ">": [ + { + "count": [ + { + "var": "singleVisual.vcObjects.border" + } + ] + }, + 0 + ] + } + ] + } + ] + }, + { + "torecord": [ + "name", + { + "var": "name" + }, + "borderProperties", + { + "var": "singleVisual.vcObjects.border" + } + ] + } + ] + }, + { + "visualsConfigArray": "." + }, + [] + ] + }, + { + "id": "VISUALHEADER_PROPERTIES_PRESENT", + "name": "Consider using a theme file to define visualHeader properties.", + "description": "", + "disabled": false, + "logType": "warning", + "forEachPath": "$.sections[*]", + "forEachPathName": "$.name", + "forEachPathDisplayName": "$.displayName", + "path": "$.visualContainers[*].config", + "pathErrorWhenNoMatch": false, + "test": [ + { + "map": [ + { + "filter": [ + { + "var": "visualsConfigArray" + }, + { + "and": [ + { + "!": [ + { + "in": [ + { + "var": "singleVisual.visualType" + }, + [ + "specify comma separated list of excluded visual types here" + ] + ] + } + ] + }, + { + "!": [ + { + "in": [ + { + "var": "name" + }, + [ + "specify comma separated list of excluded visual names here" + ] + ] + } + ] + }, + { + ">": [ + { + "count": [ + { + "var": "singleVisual.vcObjects.visualHeader" + } + ] + }, + 0 + ] + } + ] + } + ] + }, + { + "torecord": [ + "name", + { + "var": "name" + }, + "visualHeaderProperties", + { + "var": "singleVisual.vcObjects.visualHeader" + } + ] + } + ] + }, + { + "visualsConfigArray": "." + }, + [] + ] + }, + { + "id": "VISUALTOOLTIP_PROPERTIES_PRESENT", + "name": "Consider using a theme file to define visualTooltip properties.", + "description": "", + "disabled": false, + "logType": "warning", + "forEachPath": "$.sections[*]", + "forEachPathName": "$.name", + "forEachPathDisplayName": "$.displayName", + "path": "$.visualContainers[*].config", + "pathErrorWhenNoMatch": false, + "test": [ + { + "map": [ + { + "filter": [ + { + "var": "visualsConfigArray" + }, + { + "and": [ + { + "!": [ + { + "in": [ + { + "var": "singleVisual.visualType" + }, + [ + "specify comma separated list of excluded visual types here" + ] + ] + } + ] + }, + { + "!": [ + { + "in": [ + { + "var": "name" + }, + [ + "specify comma separated list of excluded visual names here" + ] + ] + } + ] + }, + { + ">": [ + { + "count": [ + { + "var": "singleVisual.vcObjects.visualTooltip" + } + ] + }, + 0 + ] + } + ] + } + ] + }, + { + "torecord": [ + "name", + { + "var": "name" + }, + "visualTooltipProperties", + { + "var": "singleVisual.vcObjects.visualTooltip" + } + ] + } + ] + }, + { + "visualsConfigArray": "." + }, + [] + ] + }, + { + "id": "PADDING_PROPERTIES_PRESENT", + "name": "Consider using a theme file to define padding properties.", + "description": "", + "disabled": false, + "logType": "warning", + "forEachPath": "$.sections[*]", + "forEachPathName": "$.name", + "forEachPathDisplayName": "$.displayName", + "path": "$.visualContainers[*].config", + "pathErrorWhenNoMatch": false, + "test": [ + { + "map": [ + { + "filter": [ + { + "var": "visualsConfigArray" + }, + { + "and": [ + { + "!": [ + { + "in": [ + { + "var": "singleVisual.visualType" + }, + [ + "specify comma separated list of excluded visual types here" + ] + ] + } + ] + }, + { + "!": [ + { + "in": [ + { + "var": "name" + }, + [ + "specify comma separated list of excluded visual names here" + ] + ] + } + ] + }, + { + ">": [ + { + "count": [ + { + "var": "singleVisual.vcObjects.padding" + } + ] + }, + 0 + ] + } + ] + } + ] + }, + { + "torecord": [ + "name", + { + "var": "name" + }, + "paddingProperties", + { + "var": "singleVisual.vcObjects.padding" + } + ] + } + ] + }, + { + "visualsConfigArray": "." + }, + [] + ] + }, + { + "id": "BACKGROUND_PROPERTIES_PRESENT", + "name": "Consider using a theme file to define background properties.", + "description": "", + "disabled": false, + "logType": "warning", + "forEachPath": "$.sections[*]", + "forEachPathName": "$.name", + "forEachPathDisplayName": "$.displayName", + "path": "$.visualContainers[*].config", + "pathErrorWhenNoMatch": false, + "test": [ + { + "map": [ + { + "filter": [ + { + "var": "visualsConfigArray" + }, + { + "and": [ + { + "!": [ + { + "in": [ + { + "var": "singleVisual.visualType" + }, + [ + "specify comma separated list of excluded visual types here" + ] + ] + } + ] + }, + { + "!": [ + { + "in": [ + { + "var": "name" + }, + [ + "specify comma separated list of excluded visual names here" + ] + ] + } + ] + }, + { + ">": [ + { + "count": [ + { + "var": "singleVisual.vcObjects.background" + } + ] + }, + 0 + ] + } + ] + } + ] + }, + { + "torecord": [ + "name", + { + "var": "name" + }, + "backgroundProperties", + { + "var": "singleVisual.vcObjects.background" + } + ] + } + ] + }, + { + "visualsConfigArray": "." + }, + [] + ] + }, + { + "id": "DROPSHADOW_PROPERTIES_PRESENT", + "name": "Consider using a theme file to define dropShadow properties.", + "description": "", + "disabled": false, + "logType": "warning", + "forEachPath": "$.sections[*]", + "forEachPathName": "$.name", + "forEachPathDisplayName": "$.displayName", + "path": "$.visualContainers[*].config", + "pathErrorWhenNoMatch": false, + "test": [ + { + "map": [ + { + "filter": [ + { + "var": "visualsConfigArray" + }, + { + "and": [ + { + "!": [ + { + "in": [ + { + "var": "singleVisual.visualType" + }, + [ + "specify comma separated list of excluded visual types here" + ] + ] + } + ] + }, + { + "!": [ + { + "in": [ + { + "var": "name" + }, + [ + "specify comma separated list of excluded visual names here" + ] + ] + } + ] + }, + { + ">": [ + { + "count": [ + { + "var": "singleVisual.vcObjects.dropShadow" + } + ] + }, + 0 + ] + } + ] + } + ] + }, + { + "torecord": [ + "name", + { + "var": "name" + }, + "dropShadowProperties", + { + "var": "singleVisual.vcObjects.dropShadow" + } + ] + } + ] + }, + { + "visualsConfigArray": "." + }, + [] + ] + }, + { + "id": "TITLE_PROPERTIES_PRESENT", + "name": "Consider using a theme file to define title properties.", + "description": "", + "disabled": false, + "logType": "warning", + "forEachPath": "$.sections[*]", + "forEachPathName": "$.name", + "forEachPathDisplayName": "$.displayName", + "path": "$.visualContainers[*].config", + "pathErrorWhenNoMatch": false, + "test": [ + { + "map": [ + { + "filter": [ + { + "var": "visualsConfigArray" + }, + { + "and": [ + { + "!": [ + { + "in": [ + { + "var": "singleVisual.visualType" + }, + [ + "specify comma separated list of excluded visual types here" + ] + ] + } + ] + }, + { + "!": [ + { + "in": [ + { + "var": "name" + }, + [ + "specify comma separated list of excluded visual names here" + ] + ] + } + ] + }, + { + ">": [ + { + "count": [ + { + "filter": [ + { + "var": "singleVisual.vcObjects.title" + }, + { "!": { "var": "properties.text" } } + ] + } + ] + }, + 0 + ] + } + ] + } + ] + }, + { + "torecord": [ + "name", + { + "var": "name" + }, + "titleProperties", + { + "filter": [ + { + "var": "singleVisual.vcObjects.title" + }, + { "!": { "var": "properties.text" } } + ] + } + ] + } + ] + }, + { + "visualsConfigArray": "." + }, + [] + ] + }, + { + "id": "SUBTITLE_PROPERTIES_PRESENT", + "name": "Consider using a theme file to define subtitle properties.", + "description": "", + "disabled": false, + "logType": "warning", + "forEachPath": "$.sections[*]", + "forEachPathName": "$.name", + "forEachPathDisplayName": "$.displayName", + "path": "$.visualContainers[*].config", + "pathErrorWhenNoMatch": false, + "test": [ + { + "map": [ + { + "filter": [ + { + "var": "visualsConfigArray" + }, + { + "and": [ + { + "!": [ + { + "in": [ + { + "var": "singleVisual.visualType" + }, + [ + "specify comma separated list of excluded visual types here" + ] + ] + } + ] + }, + { + "!": [ + { + "in": [ + { + "var": "name" + }, + [ + "specify comma separated list of excluded visual names here" + ] + ] + } + ] + }, + { + ">": [ + { + "count": [ + { + "var": "singleVisual.vcObjects.subtitle" + } + ] + }, + 0 + ] + } + ] + } + ] + }, + { + "torecord": [ + "name", + { + "var": "name" + }, + "subtitleProperties", + { + "var": "singleVisual.vcObjects.subtitle" + } + ] + } + ] + }, + { + "visualsConfigArray": "." + }, + [] + ] + }, + { + "id": "SPACING_PROPERTIES_PRESENT", + "name": "Consider using a theme file to define spacing properties.", + "description": "", + "disabled": false, + "logType": "warning", + "forEachPath": "$.sections[*]", + "forEachPathName": "$.name", + "forEachPathDisplayName": "$.displayName", + "path": "$.visualContainers[*].config", + "pathErrorWhenNoMatch": false, + "test": [ + { + "map": [ + { + "filter": [ + { + "var": "visualsConfigArray" + }, + { + "and": [ + { + "!": [ + { + "in": [ + { + "var": "singleVisual.visualType" + }, + [ + "specify comma separated list of excluded visual types here" + ] + ] + } + ] + }, + { + "!": [ + { + "in": [ + { + "var": "name" + }, + [ + "specify comma separated list of excluded visual names here" + ] + ] + } + ] + }, + { + ">": [ + { + "count": [ + { + "var": "singleVisual.vcObjects.spacing" + } + ] + }, + 0 + ] + } + ] + } + ] + }, + { + "torecord": [ + "name", + { + "var": "name" + }, + "spacingProperties", + { + "var": "singleVisual.vcObjects.spacing" + } + ] + } + ] + }, + { + "visualsConfigArray": "." + }, + [] + ] + }, + { + "id": "DIVIDER_PROPERTIES_PRESENT", + "name": "Consider using a theme file to define divider properties.", + "description": "", + "disabled": false, + "logType": "warning", + "forEachPath": "$.sections[*]", + "forEachPathName": "$.name", + "forEachPathDisplayName": "$.displayName", + "path": "$.visualContainers[*].config", + "pathErrorWhenNoMatch": false, + "test": [ + { + "map": [ + { + "filter": [ + { + "var": "visualsConfigArray" + }, + { + "and": [ + { + "!": [ + { + "in": [ + { + "var": "singleVisual.visualType" + }, + [ + "specify comma separated list of excluded visual types here" + ] + ] + } + ] + }, + { + "!": [ + { + "in": [ + { + "var": "name" + }, + [ + "specify comma separated list of excluded visual names here" + ] + ] + } + ] + }, + { + ">": [ + { + "count": [ + { + "var": "singleVisual.vcObjects.divider" + } + ] + }, + 0 + ] + } + ] + } + ] + }, + { + "torecord": [ + "name", + { + "var": "name" + }, + "dividerProperties", + { + "var": "singleVisual.vcObjects.divider" + } + ] + } + ] + }, + { + "visualsConfigArray": "." + }, + [] + ] + }, + { + "id": "STYLEPRESET_PROPERTIES_PRESENT", + "name": "Consider using a theme file to define stylePreset properties.", + "description": "", + "disabled": false, + "logType": "warning", + "forEachPath": "$.sections[*]", + "forEachPathName": "$.name", + "forEachPathDisplayName": "$.displayName", + "path": "$.visualContainers[*].config", + "pathErrorWhenNoMatch": false, + "test": [ + { + "map": [ + { + "filter": [ + { + "var": "visualsConfigArray" + }, + { + "and": [ + { + "!": [ + { + "in": [ + { + "var": "singleVisual.visualType" + }, + [ + "specify comma separated list of excluded visual types here" + ] + ] + } + ] + }, + { + "!": [ + { + "in": [ + { + "var": "name" + }, + [ + "specify comma separated list of excluded visual names here" + ] + ] + } + ] + }, + { + ">": [ + { + "count": [ + { + "var": "singleVisual.vcObjects.stylePreset" + } + ] + }, + 0 + ] + } + ] + } + ] + }, + { + "torecord": [ + "name", + { + "var": "name" + }, + "stylePresetProperties", + { + "var": "singleVisual.vcObjects.stylePreset" + } + ] + } + ] + }, + { + "visualsConfigArray": "." + }, + [] + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/PBIXInspector.sln b/PBIXInspector.sln index bc8a40d..16b9ee8 100644 --- a/PBIXInspector.sln +++ b/PBIXInspector.sln @@ -42,6 +42,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DocsExamples", "DocsExample DocsExamples\Example-ReportPageFieldMap.json = DocsExamples\Example-ReportPageFieldMap.json DocsExamples\Example-rules.json = DocsExamples\Example-rules.json DocsExamples\Reid-rules.json = DocsExamples\Reid-rules.json + DocsExamples\Themeable-properties-rules.json = DocsExamples\Themeable-properties-rules.json EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Rules", "Rules", "{BB86A63C-BBD5-48DA-9481-343165781823}" diff --git a/PBIXInspectorCLI/PBIXInspectorCLI.csproj b/PBIXInspectorCLI/PBIXInspectorCLI.csproj index 225d664..4df2d37 100644 --- a/PBIXInspectorCLI/PBIXInspectorCLI.csproj +++ b/PBIXInspectorCLI/PBIXInspectorCLI.csproj @@ -8,9 +8,9 @@ Program False True - 1.9.4.0 - 1.9.4 - 1.9.4 + 1.9.5.0 + 1.9.5 + 1.9.5 $(VersionPrefix) $(AssembblyName) pbiinspector.ico diff --git a/PBIXInspectorLibrary/InspectionRules.cs b/PBIXInspectorLibrary/InspectionRules.cs index 04147ca..23e2154 100644 --- a/PBIXInspectorLibrary/InspectionRules.cs +++ b/PBIXInspectorLibrary/InspectionRules.cs @@ -34,6 +34,8 @@ public IEnumerable EnabledRules public class Rule { + public string Id { get; set; } + public string Name { get; set; } public string Description { get; set; } diff --git a/PBIXInspectorLibrary/Inspector.cs b/PBIXInspectorLibrary/Inspector.cs index c937882..54675e1 100644 --- a/PBIXInspectorLibrary/Inspector.cs +++ b/PBIXInspectorLibrary/Inspector.cs @@ -73,22 +73,7 @@ public Inspector(string pbiFilePath, string rulesFilePath) : base(pbiFilePath, r AddCustomRulesToRegistry(); } - private PbiFile InitPbiFile(string pbiFilePath) - { - switch (PbiFile.PBIFileType(pbiFilePath)) - { - case PbiFile.PBIFileTypeEnum.PBIX: - return new PbixFile(pbiFilePath); - break; - case PbiFile.PBIFileTypeEnum.PBIP: - return new PbipFile(pbiFilePath); - break; - case PbiFile.PBIFileTypeEnum.PBIPReport: - return new PbipReportFile(pbiFilePath); - default: - throw new PBIXInspectorException(string.Format("Could not determine the extension of PBI file with path \"{0}\".", pbiFilePath)); - } - } + private void AddCustomRulesToRegistry() { @@ -124,7 +109,7 @@ public IEnumerable Inspect() { var testResults = new List(); - using (var pbiFile = InitPbiFile(_pbiFilePath)) + using (var pbiFile = PbiFileUtils.InitPbiFile(_pbiFilePath)) { foreach (var entry in this._inspectionRules.PbiEntries) { @@ -228,11 +213,11 @@ public IEnumerable Inspect() string resultString = string.Concat("\"", strForEachDisplayName, "\" - ", string.Format("Rule \"{0}\" {1} with result: {2}, expected: {3}.", rule != null ? rule.Name : string.Empty, result ? "PASSED" : "FAILED", jruleresult != null ? jruleresult.ToString() : string.Empty, rule.Test.Expected != null ? rule.Test.Expected.ToString() : string.Empty)); //yield return new TestResult { RuleName = rule.Name, ParentName = strForEachName, ParentDisplayName = strForEachDisplayName, Pass = result, Message = resultString, Expected = rule.Test.Expected, Actual = jruleresult}; - testResults.Add(new TestResult { RuleName = rule.Name, LogType = ruleLogType, RuleDescription = rule.Description, ParentName = strForEachName, ParentDisplayName = strForEachDisplayName, Pass = result, Message = resultString, Expected = rule.Test.Expected, Actual = jruleresult }); + testResults.Add(new TestResult { RuleId = rule.Id, RuleName = rule.Name, LogType = ruleLogType, RuleDescription = rule.Description, ParentName = strForEachName, ParentDisplayName = strForEachDisplayName, Pass = result, Message = resultString, Expected = rule.Test.Expected, Actual = jruleresult }); } catch (PBIXInspectorException e) { - testResults.Add(new TestResult { RuleName = rule.Name, LogType = MessageTypeEnum.Error, RuleDescription = rule.Description, ParentName = strForEachName, ParentDisplayName = strForEachDisplayName, Pass = false, Message = e.Message, Expected = rule.Test.Expected, Actual = null }); + testResults.Add(new TestResult { RuleId = rule.Id, RuleName = rule.Name, LogType = MessageTypeEnum.Error, RuleDescription = rule.Description, ParentName = strForEachName, ParentDisplayName = strForEachDisplayName, Pass = false, Message = e.Message, Expected = rule.Test.Expected, Actual = null }); continue; } } @@ -260,11 +245,11 @@ public IEnumerable Inspect() string resultString = string.Format("Rule \"{0}\" {1} with result: {2}, expected: {3}.", rule != null ? rule.Name : string.Empty, result ? "PASSED" : "FAILED", jruleresult != null ? jruleresult.ToString() : string.Empty, rule.Test.Expected != null ? rule.Test.Expected.ToString() : string.Empty); //yield return new TestResult { RuleName = rule.Name, Pass = result, Message = resultString, Expected = rule.Test.Expected, Actual = jruleresult }; - testResults.Add(new TestResult { RuleName = rule.Name, LogType = ruleLogType, RuleDescription = rule.Description, Pass = result, Message = resultString, Expected = rule.Test.Expected, Actual = jruleresult }); + testResults.Add(new TestResult { RuleId = rule.Id, RuleName = rule.Name, LogType = ruleLogType, RuleDescription = rule.Description, Pass = result, Message = resultString, Expected = rule.Test.Expected, Actual = jruleresult }); } catch (PBIXInspectorException e) { - testResults.Add(new TestResult { RuleName = rule.Name, LogType = MessageTypeEnum.Error, RuleDescription = rule.Description, Pass = false, Message = e.Message, Expected = rule.Test.Expected, Actual = null }); + testResults.Add(new TestResult { RuleId = rule.Id, RuleName = rule.Name, LogType = MessageTypeEnum.Error, RuleDescription = rule.Description, Pass = false, Message = e.Message, Expected = rule.Test.Expected, Actual = null }); continue; } } diff --git a/PBIXInspectorLibrary/Output/TestResult.cs b/PBIXInspectorLibrary/Output/TestResult.cs index 8ddd50f..cccf08a 100644 --- a/PBIXInspectorLibrary/Output/TestResult.cs +++ b/PBIXInspectorLibrary/Output/TestResult.cs @@ -6,6 +6,8 @@ public class TestResult { public Guid Id { get; private set; } + public string? RuleId { get; set; } + public string RuleName { get; set; } public string? RuleDescription { get; set; } diff --git a/PBIXInspectorLibrary/PbiFileUtils.cs b/PBIXInspectorLibrary/PbiFileUtils.cs new file mode 100644 index 0000000..24846da --- /dev/null +++ b/PBIXInspectorLibrary/PbiFileUtils.cs @@ -0,0 +1,26 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleToAttribute("PBIXInspectorTests")] + +namespace PBIXInspectorLibrary +{ + internal class PbiFileUtils + { + internal static PbiFile InitPbiFile(string pbiFilePath) + { + if (string.IsNullOrEmpty(pbiFilePath)) throw new PBIXInspectorException("PBI file path is empty."); + + switch (PbiFile.PBIFileType(pbiFilePath)) + { + case PbiFile.PBIFileTypeEnum.PBIX: + return new PbixFile(pbiFilePath); + case PbiFile.PBIFileTypeEnum.PBIP: + return new PbipFile(pbiFilePath); + case PbiFile.PBIFileTypeEnum.PBIPReport: + return new PbipReportFile(pbiFilePath); + default: + throw new PBIXInspectorException(string.Format("Could not determine the extension of PBI file with path \"{0}\".", pbiFilePath)); + } + } + } +} diff --git a/PBIXInspectorTests/CLIArgsUtilsTest.cs b/PBIXInspectorTests/CLIArgsUtilsTest.cs index 4fb8bab..dc25701 100644 --- a/PBIXInspectorTests/CLIArgsUtilsTest.cs +++ b/PBIXInspectorTests/CLIArgsUtilsTest.cs @@ -217,6 +217,24 @@ public void TestCLIArgsUtilsResolvePbiFilePathInput3() Assert.IsTrue(resolvedPath == expectedPath); } + [Test] + public void TestCLIArgsUtilsResolvePbipFilePathThrows() + { + string inputPath = @"C:\TEMP\VisOps\Sales - custom colours.pbip"; + + ArgumentException ex = Assert.Throws( + () => ArgsUtils.ResolvePbiFilePathInput(inputPath)); + } + + [Test] + public void TestCLIArgsUtilsResolvePbirFilePathThrows() + { + string inputPath = @"C:\TEMP\VisOps\Sales - custom colours.Report\definition.pbir"; + + ArgumentException ex = Assert.Throws( + () => ArgsUtils.ResolvePbiFilePathInput(inputPath)); + } + [Test] public void TestCLIArgsUtilsSuccess_FormatsOption() { diff --git a/PBIXInspectorTests/Files/Inventory sample - fails.pbix b/PBIXInspectorTests/Files/Base-rules-fails.pbix similarity index 100% rename from PBIXInspectorTests/Files/Inventory sample - fails.pbix rename to PBIXInspectorTests/Files/Base-rules-fails.pbix diff --git a/PBIXInspectorTests/Files/Inventory sample - passes.pbix b/PBIXInspectorTests/Files/Base-rules-passes.pbix similarity index 100% rename from PBIXInspectorTests/Files/Inventory sample - passes.pbix rename to PBIXInspectorTests/Files/Base-rules-passes.pbix diff --git a/PBIXInspectorTests/Files/Example-rules-fails.pbix b/PBIXInspectorTests/Files/Example-rules-fails.pbix new file mode 100644 index 0000000..288f7a0 Binary files /dev/null and b/PBIXInspectorTests/Files/Example-rules-fails.pbix differ diff --git a/PBIXInspectorTests/PBIXInspectorTests.csproj b/PBIXInspectorTests/PBIXInspectorTests.csproj index 850c4dd..a62d17b 100644 --- a/PBIXInspectorTests/PBIXInspectorTests.csproj +++ b/PBIXInspectorTests/PBIXInspectorTests.csproj @@ -30,6 +30,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -62,16 +65,19 @@ + + PreserveNewest + PreserveNewest PreserveNewest - + PreserveNewest - + PreserveNewest diff --git a/PBIXInspectorTests/PbiFileUtilsTests.cs b/PBIXInspectorTests/PbiFileUtilsTests.cs new file mode 100644 index 0000000..dd3a8e1 --- /dev/null +++ b/PBIXInspectorTests/PbiFileUtilsTests.cs @@ -0,0 +1,36 @@ +#pragma warning disable CS8602 + +using PBIXInspectorLibrary; + +namespace PBIXInspectorTests +{ + public class PbiFileUtilsTests + { + [Test] + public void PbiFileUtilsThrows() + { + PbiFile pbiFile = null; + string pbiFilePath = null; + PBIXInspectorException ex = Assert.Throws( + () => pbiFile = PbiFileUtils.InitPbiFile(pbiFilePath)); + } + + [Test] + public void PbiFileUtilsThrows2() + { + PbiFile pbiFile = null; + string pbiFilePath = string.Empty; + PBIXInspectorException ex = Assert.Throws( + () => pbiFile = PbiFileUtils.InitPbiFile(pbiFilePath)); + } + + [Test] + public void PbiFileUtilsThrows3() + { + PbiFile pbiFile = null; + string pbiFilePath = "myreport.xxxx"; + PBIXInspectorException ex = Assert.Throws( + () => pbiFile = PbiFileUtils.InitPbiFile(pbiFilePath)); + } + } +} diff --git a/PBIXInspectorTests/SuiteRunner.cs b/PBIXInspectorTests/SuiteRunner.cs index e77770f..2baca54 100644 --- a/PBIXInspectorTests/SuiteRunner.cs +++ b/PBIXInspectorTests/SuiteRunner.cs @@ -69,7 +69,7 @@ public void RunPbipTest(TestResult testResult) #region BasePassSuite public static IEnumerable BasePassPBIXSuite() { - string PBIXFilePath = @"Files\Inventory sample - passes.pbix"; + string PBIXFilePath = @"Files\Base-rules-passes.pbix"; string RulesFilePath = @"Files\Base-rules.json"; Console.WriteLine("Running base pass PBIX suite..."); @@ -86,7 +86,7 @@ public void RunBasePassPBIX(TestResult testResult) #region BaseFailSuite public static IEnumerable BaseFailPBIXSuite() { - string PBIXFilePath = @"Files\Inventory sample - fails.pbix"; + string PBIXFilePath = @"Files\Base-rules-fails.pbix"; string RulesFilePath = @"Files\Base-rules.json"; Console.WriteLine("Running base fail PBIX suite..."); @@ -117,13 +117,13 @@ public void RunBaseFailPBIP(TestResult testResult) private void RunBaseFail(TestResult testResult) { string expected = "[]"; - switch (testResult.RuleName) + switch (testResult.RuleId) { - case "Remove custom visuals which are not used in the report.": + case "REMOVE_UNUSED_CUSTOM_VISUALS": expected = "[\"Aquarium1442671919391\"]"; JsonAssert.AreEquivalent(testResult.Actual, JsonNode.Parse(expected)); break; - case "Reduce the number of visible visuals on the page": + case "REDUCE_VISUALS_ON_PAGE": if (testResult.ParentName == "ReportSectionfb0835fa991786b43a3f") { Assert.False(testResult.Pass, testResult.Message); @@ -133,7 +133,7 @@ private void RunBaseFail(TestResult testResult) Assert.True(testResult.Pass, testResult.Message); } break; - case "Reduce the number of objects within visuals": + case "REDUCE_OBJECTS_WITHIN_VISUALS": if (testResult.ParentName == "ReportSection4602098ba1ff5a3805a9") { Assert.False(testResult.Pass, testResult.Message); @@ -143,7 +143,7 @@ private void RunBaseFail(TestResult testResult) Assert.True(testResult.Pass, testResult.Message); } break; - case "Reduce usage of TopN filtering visuals by page": + case "REDUCE_TOPN_FILTERS": if (testResult.ParentName == "ReportSection3440cc1dc4ec63ca3d06") { Assert.False(testResult.Pass, testResult.Message); @@ -153,7 +153,7 @@ private void RunBaseFail(TestResult testResult) Assert.True(testResult.Pass, testResult.Message); } break; - case "Reduce usage of Advanced filtering visuals by page": + case "REDUCE_ADVANCED_FILTERS": if (testResult.ParentName == "ReportSectiond7d52b137add50d28b88") { Assert.False(testResult.Pass, testResult.Message); @@ -163,10 +163,10 @@ private void RunBaseFail(TestResult testResult) Assert.True(testResult.Pass, testResult.Message); } break; - case "Reduce number of pages per report": + case "REDUCE_PAGES": Assert.True(testResult.Pass, testResult.Message); break; - case "Avoid setting ‘Show items with no data’ on columns": + case "AVOID_SHOW_ITEMS_WITH_NO_DATA": expected = "[\"797168e1f1e7658ceae6\",\"97ad01a2b8fbfca3220c\"]"; if (testResult.ParentName == "ReportSection5f326c8a8185db501ad9") { @@ -177,7 +177,7 @@ private void RunBaseFail(TestResult testResult) Assert.True(testResult.Pass, testResult.Message); } break; - case "Tooltip and Drillthrough pages should be hidden": + case "HIDE_TOOLTIP_DRILLTROUGH_PAGES": if (testResult.ParentName == "ReportSectionadc267c0d12e40458799" || testResult.ParentName == "ReportSection8952e5fd70dcea579d3b") { @@ -188,7 +188,7 @@ private void RunBaseFail(TestResult testResult) Assert.True(testResult.Pass, testResult.Message); } break; - case "Ensure charts use theme colours": + case "ENSURE_THEME_COLOURS": if (testResult.ParentName == "ReportSection6c3c3f97279fafdeeb57") { expected = "[\"1a67964cf02b6170c3b8\"]"; @@ -199,11 +199,133 @@ private void RunBaseFail(TestResult testResult) Assert.True(testResult.Pass, testResult.Message); } break; - case "Ensure pages do not scroll vertically": + case "ENSURE_PAGES_DO_NOT_SCROLL_VERTICALLY": expected = "[\"Scrolling page\"]"; JsonAssert.AreEquivalent(testResult.Actual, JsonNode.Parse(expected)); break; - case "Ensure alternativeText has been defined for all visuals": + case "ENSURE_ALTTEXT": + Assert.False(testResult.Pass, testResult.Message); + break; + default: + Assert.True(testResult.Pass, testResult.Message); + break; + } + } + #endregion + + #region ExampleFailSuite + + public static IEnumerable ExampleFailPBIXSuite() + { + string PBIXFilePath = @"Files\Example-rules-fails.pbix"; + string RulesFilePath = @"Files\Example-rules.json"; + + Console.WriteLine("Running example fail PBIX suite..."); + return Suite(PBIXFilePath, RulesFilePath); + } + + [TestCaseSource(nameof(ExampleFailPBIXSuite))] + public void RunExampleFailPBIX(TestResult testResult) + { + RunExampleFail(testResult); + } + + private void RunExampleFail(TestResult testResult) + { + string expected = "[]"; + switch (testResult.RuleId) + { + case "CHARTS_WIDER_THAN_TALL": + if (testResult.ParentDisplayName == testResult.RuleId) + { + expected = "[\"3f7d302598c1e81e7e78\", \"5094f3ff553da63e610e\"]"; + JsonAssert.AreEquivalent(JsonNode.Parse(expected), testResult.Actual); + Assert.False(testResult.Pass, testResult.Message); + } + else + { + Assert.True(testResult.Pass, testResult.Message); + } + break; + case "DISABLE_SLOW_DATASOURCE_SETTINGS": + Assert.False(testResult.Pass, testResult.Message); + break; + case "LOCAL_REPORT_SETTINGS": + Assert.False(testResult.Pass, testResult.Message); + break; + case "SHOW_AXES_TITLES": + if (testResult.ParentDisplayName == testResult.RuleId) + { + expected = "[\"a9243890e8b7ec111322\", \"d65c53d5b679c4cacba0\", \"8a0d8392a2400e899bcc\"]"; + JsonAssert.AreEquivalent(JsonNode.Parse(expected), testResult.Actual); + Assert.False(testResult.Pass, testResult.Message); + } + else + { + Assert.True(testResult.Pass, testResult.Message); + } + break; + case "PERCENTAGE_OF_CHARTS_USING_CUSTOM_COLOURS": + //if (testResult.ParentName == "ReportSectiond7d52b137add50d28b88") + //{ + // Assert.False(testResult.Pass, testResult.Message); + //} + //else + //{ + // Assert.True(testResult.Pass, testResult.Message); + //} + break; + case "ENSURE_ALT_TEXT_DEFINED_FOR_VISUALS": + expected = "[\"9032ab70a7e060d08574\",\"eca6ff83ecb390801c3a\"]"; + if (testResult.ParentDisplayName == testResult.RuleId) + { + JsonAssert.AreEquivalent(JsonNode.Parse(expected), testResult.Actual); + Assert.False(testResult.Pass, testResult.Message); + } + + break; + case "DISABLE_DROP_SHADOWS_ON_VISUALS": + expected = "[\"bdb3c2666ac0e67947aa\",\"5d4868734a72096e0ada\"]"; + if (testResult.ParentDisplayName == testResult.RuleId) + { + JsonAssert.AreEquivalent(JsonNode.Parse(expected), testResult.Actual); + Assert.False(testResult.Pass, testResult.Message); + } + else + { + Assert.True(testResult.Pass, testResult.Message); + } + break; + case "GIVE_VISIBLE_PAGES_MEANINGFUL_NAMES": + if (testResult.ParentDisplayName == "Page 1") + { + Assert.False(testResult.Pass, testResult.Message); + } + else + { + Assert.False(testResult.Pass, testResult.Message); + } + break; + case "DENEB_CHARTS_PROPERTIES": + //TODO: complete this test + //Assert.False(testResult.Pass, testResult.Message); + break; + case "CHECK_FOR_VISUALS_OVERLAP": + expected = "[\"2beb787442a6d0432b4d\",\"11f540db1a90abb52cda\",\"93e80741178005eb0ab4\",\"dead16c359819062e164\"]"; + if (testResult.ParentDisplayName == testResult.RuleId) + { + JsonAssert.AreEquivalent(JsonNode.Parse(expected), testResult.Actual); + Assert.False(testResult.Pass, testResult.Message); + } + break; + case "CHECK_FOR_LOCAL_MEASURES": + //TODO: complete this test + //Assert.False(testResult.Pass, testResult.Message); + break; + case "REPORT_THEME_NAME": + Assert.False(testResult.Pass, testResult.Message); + break; + case "REPORT_THEME_TITLE_FONT": Assert.False(testResult.Pass, testResult.Message); break; default: diff --git a/PBIXInspectorWinForm/MainForm.cs b/PBIXInspectorWinForm/MainForm.cs index 627e5c3..dcddf1f 100644 --- a/PBIXInspectorWinForm/MainForm.cs +++ b/PBIXInspectorWinForm/MainForm.cs @@ -5,8 +5,7 @@ namespace PBIXInspectorWinForm { public partial class MainForm : Form { - private static Args _args; - + public MainForm() { InitializeComponent(); @@ -112,17 +111,19 @@ private void chckUseTempFiles_CheckedChanged(object sender, EventArgs e) private void btnRun_Click(object sender, EventArgs e) { Clear(); + btnRun.Enabled = false; - var pbiFilePath = ArgsUtils.ResolvePbiFilePathInput(this.txtPBIDesktopFile.Text); + + var pbiFilePath = this.txtPBIDesktopFile.Text; var rulesFilePath = this.txtRulesFilePath.Text; var outputPath = this.txtOutputDirPath.Text; - var verboseString = this.chckVerbose.Checked.ToString(); - var formatsString = string.Concat(this.chckJsonOutput.Checked ? "JSON" : string.Empty, ",", this.chckHTMLOutput.Checked ? "HTML" : string.Empty); - _args = new Args { PBIFilePath = pbiFilePath, RulesFilePath = rulesFilePath, OutputPath = outputPath, FormatsString = formatsString, VerboseString = verboseString }; + var verbose = this.chckVerbose.Checked; + var jsonOutput = this.chckJsonOutput.Checked; + var htmlOutput = this.chckHTMLOutput.Checked; - Main.Run(_args); - btnRun.Enabled = true; + Main.Run(pbiFilePath, rulesFilePath, outputPath, verbose, jsonOutput, htmlOutput); + btnRun.Enabled = true; } internal void Clear() diff --git a/PBIXInspectorWinForm/PBIXInspectorWinForm.csproj b/PBIXInspectorWinForm/PBIXInspectorWinForm.csproj index 098e01f..d941b56 100644 --- a/PBIXInspectorWinForm/PBIXInspectorWinForm.csproj +++ b/PBIXInspectorWinForm/PBIXInspectorWinForm.csproj @@ -6,9 +6,9 @@ enable true enable - 1.9.4.0 - 1.9.4 - 1.9.4 + 1.9.5.0 + 1.9.5 + 1.9.5 README.md pbiinspector.png https://github.com/NatVanG/PBIXInspector diff --git a/PBIXInspectorWinForm/PBIXInspectorWinForm.pbitool.json b/PBIXInspectorWinForm/PBIXInspectorWinForm.pbitool.json index bb3f30f..f34735a 100644 --- a/PBIXInspectorWinForm/PBIXInspectorWinForm.pbitool.json +++ b/PBIXInspectorWinForm/PBIXInspectorWinForm.pbitool.json @@ -1,5 +1,5 @@ { - "version": "1.9.4", + "version": "1.9.5", "name": "VisOps with PBI Inspector", "description": "A testing or inspection tool for the visual layer of Microsoft Power BI reports.", "path": "%PATH%", diff --git a/PBIXInspectorWinLibrary/Constants.cs b/PBIXInspectorWinLibrary/Constants.cs index dcaef10..d1f9ed4 100644 --- a/PBIXInspectorWinLibrary/Constants.cs +++ b/PBIXInspectorWinLibrary/Constants.cs @@ -8,6 +8,8 @@ public static class Constants public const string SampleRulesFilePath = @"Files\Base-rules.json"; public const string ReportPageFieldMapFilePath = @"Files\ReportPageFieldMap.json"; public const string PBIPReportJsonFileName = "report.json"; + public const string PBIPFileExtension = ".pbip"; + public const string PBIRFileExtension = ".pbir"; public const string TestRunHTMLTemplate = @"Files\html\TestRunTemplate.html"; public const string PBIInspectorPNG = @"Files\icon\pbiinspector.png"; public const string TestRunHTMLFileName = "TestRun.html"; diff --git a/PBIXInspectorWinLibrary/Drawing/ImageUtils.cs b/PBIXInspectorWinLibrary/Drawing/ImageUtils.cs index aa75da1..95a5f74 100644 --- a/PBIXInspectorWinLibrary/Drawing/ImageUtils.cs +++ b/PBIXInspectorWinLibrary/Drawing/ImageUtils.cs @@ -45,11 +45,18 @@ public static void DrawReportPages(IEnumerable fieldMapResults, IEnu var width = (int)Math.Round(f["width"].GetValue()); var visible = f["visible"].GetValue(); - var pass = !(testResult.Actual != null && testResult.Actual is JsonArray + //If a visual name is returned in the test actual array then highlight it as a test failure in the page wireframe + //A visual name can be returned either as a JsonValue or a named JsonObject (i.e. {"name": "VisualName"}) hence the "or else" operator below (i.e. "||") + var visualNameInTestActualArray = (testResult.Actual != null && testResult.Actual is JsonArray && testResult.Actual.AsArray().Any(_ => _ != null - && _ is JsonValue && _.AsValue().ToString().Equals(name))); + && _ is JsonValue && _.AsValue().ToString().Equals(name))) + || (testResult.Actual != null && testResult.Actual is JsonArray + && testResult.Actual.AsArray().Any(_ => _ != null && _ is JsonObject && _ is not JsonValue + && _["name"] != null && _["name"] is JsonValue && _["name"].AsValue().ToString().Equals(name))); + - visuals.Add(new ReportPage.VisualContainer { Name = name.ToString(), VisualType = visualType.ToString(), X = x, Y = y, Height = height, Width = width, Pass = pass, Visible = visible }); + bool visualPass = !visualNameInTestActualArray; + visuals.Add(new ReportPage.VisualContainer { Name = name.ToString(), VisualType = visualType.ToString(), X = x, Y = y, Height = height, Width = width, Pass = visualPass, Visible = visible }); } var rp = new ReportPage(pageName, pageDisplayName, pageSize, visuals); diff --git a/PBIXInspectorWinLibrary/Files/html/TestRunTemplate.html b/PBIXInspectorWinLibrary/Files/html/TestRunTemplate.html index ddda8ce..3b06935 100644 --- a/PBIXInspectorWinLibrary/Files/html/TestRunTemplate.html +++ b/PBIXInspectorWinLibrary/Files/html/TestRunTemplate.html @@ -113,6 +113,17 @@
Results
"html": [{ "<>": "strong", "class": "d-block text-gray-dark", "text": "Verbose" }, { "text": "${Verbose}" }] } ] + }, + { + "<>": "div", + "class": "d-flex text-body-secondary pt-3", + "html": [ + { + "<>": "p", + "class": "pb-3 mb-0 small lh-sm", + "html": [{ "<>": "strong", "class": "d-block text-gray-dark", "text": "Results stats" }, { "text": function () { if (this.Results != null) { return "Results count: " + this.Results.length } else { return "No results were found."} } }] + } + ] } ] }; @@ -227,7 +238,7 @@
Results
] } ] - }, { "<>": "img", "src": function () { if (this.ParentName != null) { return "PBIInspectorPNG\\" + this.Id + ".png" } else { return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwgAADsIBFShKgAAAAAxJREFUGFdj+P//PwAF/gL+pzWBhAAAAABJRU5ErkJggg==" } }, "alt": "Page wireframe", "width": function () { if (this.ParentName != null) { return "65%" } else { return "1%" } }, "height": "auto" } + }, { "<>": "img", "src": function () { if (this.ParentName != null) { return "PBIInspectorPNG\\" + this.Id + ".png" } else { return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwgAADsIBFShKgAAAAAxJREFUGFdj+P//PwAF/gL+pzWBhAAAAABJRU5ErkJggg==" } }, "alt": "Page wireframe", "width": function () { if (this.ParentName != null) { return "65%" } else { return "1%" } }, "height": function () { if (this.ParentName != null) { return "65%" } else { return "1%" } } } ] } ] diff --git a/PBIXInspectorWinLibrary/Main.cs b/PBIXInspectorWinLibrary/Main.cs index 1f789cd..b632346 100644 --- a/PBIXInspectorWinLibrary/Main.cs +++ b/PBIXInspectorWinLibrary/Main.cs @@ -39,6 +39,26 @@ private set } } + public static void Run(string pbiFilePath, string rulesFilePath, string outputPath, bool verbose, bool jsonOutput, bool htmlOutput) + { + var formatsString = string.Concat(jsonOutput ? "JSON" : string.Empty, ",", htmlOutput ? "HTML" : string.Empty); + var verboseString = verbose.ToString(); + + string resolvedPbiFilePath = string.Empty; + + try + { + resolvedPbiFilePath = ArgsUtils.ResolvePbiFilePathInput(pbiFilePath); + } + catch (ArgumentException e) + { + OnMessageIssued(MessageTypeEnum.Error, e.Message); + } + + var args = new Args { PBIFilePath = resolvedPbiFilePath, RulesFilePath = rulesFilePath, OutputPath = outputPath, FormatsString = formatsString, VerboseString = verboseString }; + + Run(args); + } public static void Run(Args args) { @@ -54,12 +74,12 @@ public static void Run(Args args) try { - _insp = new Inspector(_args.PBIFilePath, _args.RulesFilePath); + _insp = new Inspector(Main._args.PBIFilePath, Main._args.RulesFilePath); _insp.MessageIssued += Insp_MessageIssued; - _testResults = _insp.Inspect().Where(_ => (!_args.Verbose && !_.Pass) || (_args.Verbose)); + _testResults = _insp.Inspect().Where(_ => (!Main._args.Verbose && !_.Pass) || (Main._args.Verbose)); - if (_args.CONSOLEOutput || _args.ADOOutput) + if (Main._args.CONSOLEOutput || Main._args.ADOOutput) { foreach (var result in _testResults) { @@ -70,43 +90,43 @@ public static void Run(Args args) } //Ensure output dir exists - if (!_args.ADOOutput && (_args.JSONOutput || _args.HTMLOutput || _args.PNGOutput)) + if (!Main._args.ADOOutput && (Main._args.JSONOutput || Main._args.HTMLOutput || Main._args.PNGOutput)) { - if (!Directory.Exists(_args.OutputDirPath)) + if (!Directory.Exists(Main._args.OutputDirPath)) { - Directory.CreateDirectory(_args.OutputDirPath); + Directory.CreateDirectory(Main._args.OutputDirPath); } } - if (!_args.ADOOutput && (_args.JSONOutput || _args.HTMLOutput)) + if (!Main._args.ADOOutput && (Main._args.JSONOutput || Main._args.HTMLOutput)) { var outputFilePath = string.Empty; - var pbiFileNameWOextension = Path.GetFileNameWithoutExtension(_args.PBIFilePath); + var pbiFileNameWOextension = Path.GetFileNameWithoutExtension(Main._args.PBIFilePath); - if (!string.IsNullOrEmpty(_args.OutputDirPath)) + if (!string.IsNullOrEmpty(Main._args.OutputDirPath)) { - outputFilePath = Path.Combine(_args.OutputDirPath, string.Concat("TestRun_", pbiFileNameWOextension, ".json")); + outputFilePath = Path.Combine(Main._args.OutputDirPath, string.Concat("TestRun_", pbiFileNameWOextension, ".json")); } else { - throw new ArgumentException("Directory with path \"{0}\" does not exist", _args.OutputDirPath); + throw new ArgumentException("Directory with path \"{0}\" does not exist", Main._args.OutputDirPath); } - var testRun = new TestRun() { CompletionTime = DateTime.Now, TestedFilePath = _args.PBIFilePath, RulesFilePath = _args.RulesFilePath, Verbose = _args.Verbose, Results = _testResults }; + var testRun = new TestRun() { CompletionTime = DateTime.Now, TestedFilePath = Main._args.PBIFilePath, RulesFilePath = Main._args.RulesFilePath, Verbose = Main._args.Verbose, Results = _testResults }; _jsonTestRun = JsonSerializer.Serialize(testRun); - if (_args.JSONOutput) + if (Main._args.JSONOutput) { OnMessageIssued(MessageTypeEnum.Information, string.Format("Writing JSON output to file at \"{0}\".", outputFilePath)); File.WriteAllText(outputFilePath, _jsonTestRun, System.Text.Encoding.UTF8); } } - if (!_args.ADOOutput && (_args.PNGOutput || _args.HTMLOutput)) + if (!Main._args.ADOOutput && (Main._args.PNGOutput || Main._args.HTMLOutput)) { - _fieldMapInsp = new Inspector(_args.PBIFilePath, Constants.ReportPageFieldMapFilePath); + _fieldMapInsp = new Inspector(Main._args.PBIFilePath, Constants.ReportPageFieldMapFilePath); _fieldMapResults = _fieldMapInsp.Inspect(); - var outputPNGDirPath = Path.Combine(_args.OutputDirPath, Constants.PNGOutputDir); + var outputPNGDirPath = Path.Combine(Main._args.OutputDirPath, Constants.PNGOutputDir); if (Directory.Exists(outputPNGDirPath)) { @@ -121,7 +141,7 @@ public static void Run(Args args) ImageUtils.DrawReportPages(_fieldMapResults, _testResults, outputPNGDirPath); } - if (!_args.ADOOutput && _args.HTMLOutput) + if (!Main._args.ADOOutput && Main._args.HTMLOutput) { string pbiinspectorlogobase64 = string.Concat(Constants.Base64ImgPrefix, ImageUtils.ConvertBitmapToBase64(Constants.PBIInspectorPNG)); //string nowireframebase64 = string.Concat(Base64ImgPrefix, ImageUtils.ConvertBitmapToBase64(@"Files\png\nowireframe.png")); @@ -130,13 +150,13 @@ public static void Run(Args args) html = html.Replace(Constants.VersionPlaceholder, AppUtils.About(), StringComparison.OrdinalIgnoreCase); html = html.Replace(Constants.JsonPlaceholder, _jsonTestRun, StringComparison.OrdinalIgnoreCase); - var outputHTMLFilePath = Path.Combine(_args.OutputDirPath, Constants.TestRunHTMLFileName); + var outputHTMLFilePath = Path.Combine(Main._args.OutputDirPath, Constants.TestRunHTMLFileName); OnMessageIssued(MessageTypeEnum.Information, string.Format("Writing HTML output to file at \"{0}\".", outputHTMLFilePath)); File.WriteAllText(outputHTMLFilePath, html); //Results have been written to a temporary directory so show output to user automatically. - if (_args.DeleteOutputDirOnExit) + if (Main._args.DeleteOutputDirOnExit) { AppUtils.WinOpen(outputHTMLFilePath); } diff --git a/PBIXInspectorWinLibrary/PBIXInspectorWinLibrary.csproj b/PBIXInspectorWinLibrary/PBIXInspectorWinLibrary.csproj index 647bcb4..5b6dd9d 100644 --- a/PBIXInspectorWinLibrary/PBIXInspectorWinLibrary.csproj +++ b/PBIXInspectorWinLibrary/PBIXInspectorWinLibrary.csproj @@ -4,9 +4,9 @@ net6.0 enable enable - 1.9.4.0 - 1.9.4.0 - 1.9.4 + 1.9.5.0 + 1.9.5.0 + 1.9.5 diff --git a/PBIXInspectorWinLibrary/Utils/ArgsUtils.cs b/PBIXInspectorWinLibrary/Utils/ArgsUtils.cs index 51bcd30..1d5141a 100644 --- a/PBIXInspectorWinLibrary/Utils/ArgsUtils.cs +++ b/PBIXInspectorWinLibrary/Utils/ArgsUtils.cs @@ -46,10 +46,20 @@ public static Args ParseArgs(string[] args) public static string? ResolvePbiFilePathInput(string pbiFilePath) { - var resolvedPath = !string.IsNullOrEmpty(pbiFilePath) && pbiFilePath.ToLower().EndsWith(Constants.PBIPReportJsonFileName) - ? Path.GetDirectoryName(pbiFilePath) - : pbiFilePath; + var resolvedPath = pbiFilePath; + if (!string.IsNullOrEmpty(pbiFilePath) && (pbiFilePath.ToLower().EndsWith(Constants.PBIPReportJsonFileName))) + { + resolvedPath = Path.GetDirectoryName(pbiFilePath); + } + + //TODO: support PBIP file path. Need to parse the json and retrieve the report folder path. + if (!string.IsNullOrEmpty(pbiFilePath) && (pbiFilePath.ToLower().EndsWith(Constants.PBIPFileExtension) + || pbiFilePath.ToLower().EndsWith(Constants.PBIRFileExtension))) + { + throw new ArgumentException(string.Format("PBIP and PBIR file types are not yet supported. Please specify a report.json file path instead.")); + } + return resolvedPath; } } diff --git a/Rules/Base-rules.json b/Rules/Base-rules.json index 7e9a5d0..2e24f9b 100644 --- a/Rules/Base-rules.json +++ b/Rules/Base-rules.json @@ -8,6 +8,7 @@ "codepage": 1200, "rules": [ { + "id": "REMOVE_UNUSED_CUSTOM_VISUALS", "name": "Remove custom visuals which are not used in the report.", "description": "Returns an array of custom visual names to be removed if any. To disable this rule, mark it as disabled in the base rules file.", "logType": "warning", @@ -68,6 +69,7 @@ ] }, { + "id": "REDUCE_VISUALS_ON_PAGE", "name": "Reduce the number of visible visuals on the page", "description": "Reports a test fail if the rule's maximum number of visible visuals on the page is exceeded. By default the base rules file specifies 20 as the maximum, set the paramMaxVisualsPerPage parameter to change this. To disable this rule, mark it as disabled in the base rules file.", "logType": "warning", @@ -133,6 +135,7 @@ ] }, { + "id": "REDUCE_OBJECTS_WITHIN_VISUALS", "name": "Reduce the number of objects within visuals", "description": "Reports a test fail if the rule's maximum number of objects within visuals on a page is exceeded. An object is a data field that is assigned to a visual. By default, the base rules file specifies 6 as the maximum objects within a visual. To disable this rule, mark it as disabled in the base rules file.", "logType": "warning", @@ -176,6 +179,7 @@ ] }, { + "id": "REDUCE_TOPN_FILTERS", "name": "Reduce usage of TopN filtering visuals by page", "description": "Reports a test fail if the rule's maximum number of visuals using TopN filtering on a the page is exceeded. By default the base rules file specifies 4 as the maximum objects within a visual, set the paramMaxTopNFilteringPerPage to change this. To disable this rule, mark it as disabled in the base rules file.", "logType": "warning", @@ -227,6 +231,7 @@ ] }, { + "id": "REDUCE_ADVANCED_FILTERS", "name": "Reduce usage of Advanced filtering visuals by page", "description": "Reports a test fail if the rule's maximum number of visuals using Advanced filtering on a the page is exceeded. By default, the base rules file specifies 4 as the maximum objects within a visual, set the paramMaxAdvancedFilteringVisualsPerPage parameter value to change this. To disable this rule, mark it as disabled in the base rules file.", "logType": "warning", @@ -278,6 +283,7 @@ ] }, { + "id": "REDUCE_PAGES", "name": "Reduce number of pages per report", "description": "Reports a test fail if the rule's maximum number of pages per report is exceeded. By default, the base rules file specifies 10 as the maximum number of pages. Set the paramMaxNumberOfPagesPerReport parameter to change this. To disable this rule, mark it as disabled in the base rules file.", "logType": "warning", @@ -307,6 +313,7 @@ ] }, { + "id": "AVOID_SHOW_ITEMS_WITH_NO_DATA", "name": "Avoid setting ‘Show items with no data’ on columns", "description": "Returns an array of visual names which have the option ‘Show items with no data’ enabled on one or more columns. To disable this rule, mark it as disabled in the base rules file.", "logType": "warning", @@ -357,6 +364,7 @@ ] }, { + "id": "HIDE_TOOLTIP_DRILLTROUGH_PAGES", "name": "Tooltip and Drillthrough pages should be hidden", "description": "Reports a test fail if a page of type Tooltip or Drillthrough is visible. To disable this rule, mark it as disabled in the base rules file.", "logType": "warning", @@ -404,6 +412,7 @@ ] }, { + "id": "ENSURE_THEME_COLOURS", "name": "Ensure charts use theme colours", "description": "Check that charts (excluding textboxes) avoid custom colours and use theme colours instead. To disable this rule, mark it as disabled in the base rules file.", "disabled": false, @@ -465,6 +474,7 @@ ] }, { + "id": "ENSURE_PAGES_DO_NOT_SCROLL_VERTICALLY", "name": "Ensure pages do not scroll vertically", "description": "Returns an array of visible page names with a height great than 720px. Modify the rule parameter value if a different maximum height value is required. To disable this rule, mark it as disabled in the base rules file.", "disabled": false, @@ -516,6 +526,7 @@ ] }, { + "id": "ENSURE_ALTTEXT", "name": "Ensure alternativeText has been defined for all visuals", "description": "Alt-text is required for screen readers", "disabled": true, @@ -585,6 +596,7 @@ ] }, { + "id": "template", "name": "Rule Template", "description": "Rule template", "disabled": true,