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

add test for ignored & templated unittests #861

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/dscanner/analysis/config.d
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ struct StaticAnalysisConfig
@INI("Maximum cyclomatic complexity after which to issue warnings")
int max_cyclomatic_complexity = 50;

@INI("Check for ignored unittests in templates or functions")
string ignored_unittest = Check.enabled;

@INI("Module-specific filters")
ModuleFilters filters;
}
Expand Down
188 changes: 188 additions & 0 deletions src/dscanner/analysis/ignored_unittest.d
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
module dscanner.analysis.ignored_unittest;

import dparse.lexer;
import dparse.ast;
import dscanner.analysis.base;

import std.stdio;
import std.meta : AliasSeq;

private string scoped(string v, string val)
{
if (__ctfe)
{
return "auto _old_" ~ v ~ " = " ~ v ~ "; " ~ v ~ " = " ~ val ~ "; scope (exit) " ~ v ~ " = _old_" ~ v ~ ";";
}
else
{
return null;
}
}

/**
* unittests in methods are never run, unittests in templates may only serve for documentation
*/
final class IgnoredUnittestCheck : BaseAnalyzer
{
enum string KEY_IGNORED = "dscanner.bugs.ignored_unittest";
enum string MESSAGE_IGNORED = "unittest blocks can only be run in global scope or in types";
enum string KEY_TEMPLATED = "dscanner.suspicious.templated_unittest";
enum string MESSAGE_TEMPLATED = "unittest blocks in templates may not be run and can only serve for documentation purpose. Document with a ddoc-comment or move unittest block out of template.";
mixin AnalyzerInfo!"ignored_unittest";

private bool inValidScope = true;
private bool inUndocumentable;
private bool inTemplate;

///
this(string fileName, bool skipTests = false)
{
super(fileName, null, skipTests);
}

static foreach (T; AliasSeq!(
ClassDeclaration,
InterfaceDeclaration,
StructDeclaration,
UnionDeclaration
))
override void visit(const T b)
{
mixin(scoped("inValidScope", "true"));
mixin(scoped("inTemplate", "inTemplate || b.templateParameters !is null"));
b.accept(this);
}

override void visit(const MixinTemplateDeclaration m)
{
// ignore mixin templates as they might be used for proper unittests
if (m.templateDeclaration)
{
mixin(scoped("inValidScope", "true"));
mixin(scoped("inTemplate", "false"));
m.templateDeclaration.accept(this);
}
}

override void visit(const FunctionDeclaration b)
{
mixin(scoped("inTemplate", "inTemplate || b.templateParameters !is null"));
b.accept(this);
}

override void visit(const FunctionBody b)
{
mixin(scoped("inValidScope", "false"));
mixin(scoped("inUndocumentable", "true"));
b.accept(this);
}

override void visit(const TemplateDeclaration decl)
{
mixin(scoped("inTemplate", "true"));
decl.accept(this);
}

override void visit(const Unittest test)
{
if (!inValidScope)
{
addErrorMessage(test.line, test.column, KEY_IGNORED, MESSAGE_IGNORED);
}
else if (inTemplate && inUndocumentable)
{
addErrorMessage(test.line, test.column, KEY_IGNORED, MESSAGE_IGNORED);
}
else if (inTemplate)
{
if (test.comment is null)
addErrorMessage(test.line, test.column, KEY_TEMPLATED, MESSAGE_TEMPLATED);
}

if (!skipTests)
{
mixin(scoped("inValidScope", "false"));
mixin(scoped("inUndocumentable", "true"));
test.accept(this);
}
}

alias visit = BaseAnalyzer.visit;
}

unittest
{
import std.stdio : stderr;
import std.format : format;
import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
import dscanner.analysis.helpers : assertAnalyzerWarnings;

StaticAnalysisConfig sac = disabledConfig();
sac.ignored_unittest = Check.enabled;

assertAnalyzerWarnings(q{
unittest {}

void foo() {
unittest {} // [warn]: %s
}

void bar()() {
unittest {} // [warn]: %s
}

class T1() {
unittest {} // [warn]: %s
}

class T2() {
///
unittest {}
}

class C {
unittest {}
}

unittest {
unittest {} // [warn]: %s
}

void test1() {
struct S {
unittest {} // surprisingly, this gets called
}
}

void test2()() {
struct S {
unittest {} // [warn]: %s
}
}

void test2() {
struct S() {
unittest {} // [warn]: %s
}
}

mixin template MT()
{
unittest { /* ok */ }
}

struct MixedStruct
{
mixin MT;
}
}c.format(
IgnoredUnittestCheck.MESSAGE_IGNORED,
IgnoredUnittestCheck.MESSAGE_IGNORED,
IgnoredUnittestCheck.MESSAGE_TEMPLATED,
IgnoredUnittestCheck.MESSAGE_IGNORED,
IgnoredUnittestCheck.MESSAGE_IGNORED,
IgnoredUnittestCheck.MESSAGE_IGNORED,
), sac);

stderr.writeln("Unittest for IgnoredUnittestCheck passed.");
}
5 changes: 5 additions & 0 deletions src/dscanner/analysis/run.d
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import dscanner.analysis.trust_too_much;
import dscanner.analysis.redundant_storage_class;
import dscanner.analysis.unused_result;
import dscanner.analysis.cyclomatic_complexity;
import dscanner.analysis.ignored_unittest;

import dsymbol.string_interning : internString;
import dsymbol.scope_;
Expand Down Expand Up @@ -595,6 +596,10 @@ MessageSet analyze(string fileName, const Module m, const StaticAnalysisConfig a
analysisConfig.cyclomatic_complexity == Check.skipTests && !ut,
analysisConfig.max_cyclomatic_complexity.to!int);

if (moduleName.shouldRun!IgnoredUnittestCheck(analysisConfig))
checks ~= new IgnoredUnittestCheck(fileName,
analysisConfig.unused_result == Check.skipTests && !ut);

version (none)
if (moduleName.shouldRun!IfStatementCheck(analysisConfig))
checks ~= new IfStatementCheck(fileName, moduleScope,
Expand Down