0

I am working on a large project where a base class has thousands of classes derived from it (multiple developers are working on them). Each class is expected to override a set of methods. I first generated these thousands of class files with a code template that conforms to an acceptable pattern. I am now writing unit tests to ensure that developers have not deviated from this pattern. Here is a sample generated class:

// Base class.
public abstract partial class BaseClass
{
    protected abstract bool OnTest ();
}

// Derived class. DO NOT CHANGE THE CLASS NAME!
public sealed partial class DerivedClass_00000001: BaseClass
{
    /// <summary>
    /// Do not modify the code template in any way.
    /// Write code only in the try and finally blocks in this method.
    /// </summary>
    protected override void OnTest ()
    {
        bool result = false;
        ComObject com = null;
        // Declare ALL value and reference type variables here. NOWHERE ELSE!
        // Variables that would otherwise be narrowly scoped should also be declared here.
        // Initialize all reference types to [null]. [object o;] does not conform. [object o = null;] conforms.
        // Initialize all value types to their default values. [int x;] does not conform. [int x = 0;] conforms.

        try
        {
            com = new ComObject();

            // Process COM objects here.
            // Do NOT return out of this function yourself!
        }
        finally
        {
            // Release all COM objects.
            System.Runtime.InteropServices.Marshal.ReleaseComObject(com);

            // Set all COM objects to [null].
            // The base class will take care of explicit garbage collection.
            com = null;
        }

        return (result);
    }
}

In the unit tests, I have been able to verify the following via reflection:

  • The class derives from [BaseClass] and does not implement any interfaces.
  • The class name conforms to a pattern.
  • The catch block has not been filtered.
  • No other catch blocks have been added.
  • No class level fields or properties have been declared.
  • All method value type variables have been manually initialized upon declaration.
  • No other methods have been added to the derived classes.

The above is easily achieved via reflection but I am struggling with asserting the following list:

  • The catch block re-throws the caught exception rather than wrapping it or throwing some other exception.
  • The [return (result);] line at the end has not been modified and no other [return (whatever);] calls have been added. No idea how to achieve this.
  • Verify that all reference types implementing IDisposable have been disposed.
  • Verify that all reference types of type [System.__ComObject] have been manually de-referenced and set to [null] in the finally block.

I have thought about parsing the source code but I don't like that solution unless absolutely necessary. It is messy and unless I have expression trees, almost impossible to guarantee success.

Any tips would be appreciated.

Raheel Khan
  • 14,205
  • 13
  • 80
  • 168

3 Answers3

6

Some thoughts:

  1. If the methods need to be overriden, why are they virtual instead of abstract?
  2. Code that should not be changed doesn't belong in the derived class. It belongs in the base class.
  3. catch { throw; } is useless. Remove it.
  4. Returning a boolean value from a void method causes a compiler error.
  5. Setting local variables to null is useless.
  6. Not all reference types implement IDisposable.

Generally: Most of your requirements seem to have no business value.

  • Why prohibit implementation of an interface?
  • Why prohibit declaration of other methods?
  • Why prohibit catch clauses?
  • etc.

You should really think about what your actual business requirements are and model your classes after them. If the classes need to fulfill a certain contract, model that contract. Leave the implementation to the implementor.


About the actual questions raised:
You can't use reflection here. You can either analyze the original source code or the IL code of the compiled assembly.
Both options are pretty tricky and most likely impossible to achieve within your limited time. I am positive that fixing the architecture would take less time than implementing one of those options.

Daniel Hilgarth
  • 171,043
  • 40
  • 335
  • 443
  • 1
    + why having up to 100 million class names? :) – Alex Filipovici May 13 '13 at 14:23
  • I typed out a quick example so there were mistakes. Virtual fixed. Catch block removed. Return type fixed. IDisposable where it applies, of course. Moving on, this project is very large and clunky and we do not have the time or budget to meddle with architecture. These derived classes have a VERY narrow purpose and I need to ensure it conforms to these standards. Your answer does not address any of my questions. – Raheel Khan May 13 '13 at 14:47
  • @RaheelKhan: Added an explicit answer for your questions. – Daniel Hilgarth May 13 '13 at 14:53
  • Thanks. I understand why you would assume that. The trouble with my scenario is that we do not have the resources to re-architect the system. The developers implementing derived classes are entry-level programmers and the number of classes being added on a daily basis is quite large. Unfortunately, I am stuck with using conformity to pass initial code quality checks before code logic is verified and tested. I agree that IL analysis is out of the question. Continued... – Raheel Khan May 13 '13 at 15:07
  • Source code parsing can take care of most scenarios but what I am really after is to somehow obtain expression trees which can provide valuable insight. Would you agree? If yes, what is a good place to start for someone not familiar with expression trees? – Raheel Khan May 13 '13 at 15:07
  • @RaheelKhan: The problem is that you can't get expression trees from this source code. – Daniel Hilgarth May 13 '13 at 15:08
  • I can instantiate the derived classes in my unit test code. Does that help? – Raheel Khan May 13 '13 at 15:10
  • @RaheelKhan: Nope, it doesn't. Expression trees are created at *compile time* and your code simply doesn't create any expression trees. Furthermore, lambda expressions with a statement body can't be automatically converted to expression trees by the compiler. – Daniel Hilgarth May 13 '13 at 15:11
  • Drat. Well thank you for helping eliminate those options. Looks like source code analysis using RegEx and possibly some CodeDom play-around seem to be the only ways out. – Raheel Khan May 13 '13 at 15:15
  • @RaheelKhan: I disagree. Changing the architecture should be really straight forward. The resulting architecture wouldn't require more skill for implementing those derived classes than the current solution. Even with thousands of derived classes I would estimate not more than a week of effort for it. The usage doesn't have to be changed, because the contract of the base class would stay the same. – Daniel Hilgarth May 13 '13 at 15:19
  • Not in this case. The system has been built to treat class implementations as if they were dynamic data! The system does not even use dynamic library loading for new classes (a recompile is required due to static linking). This means redeployment every week. Everything from the UI to the BLL and DAL have been designed around it. Even the darn data model makes ridiculous assumptions about ID's hard coded into classes. I'd love to meet the genius who designed this system :). Fortunately, the application only has two years of life remaining. Until then, I'm stuck with it. – Raheel Khan May 13 '13 at 15:29
  • @RaheelKhan: Oops. I truly pity you. – Daniel Hilgarth May 13 '13 at 15:34
2

You could try to use Roslyn CTP here if the fully automated code analysis is what you really need. It has more advanced syntax and semantics analysis than reflection does. But it is still a lot of work. Working directly with developers, not with their code, preparing templates, guidelines may be more time efficient.

OpenMinded
  • 496
  • 3
  • 10
  • Thank you. That looks very interesting. I am exploring CodeDom but may very well go with Roslyn after some research into it. Yes, it is a lot of work and although we already have template generation and development guidelines, verifying thousands of classes for subtle pattern deviations is a nightmare unless you have some level of confidence from an automated analysis. From there on, logic verification along with testing takes comparatively very little time. – Raheel Khan May 13 '13 at 17:30
0

While I'm sure you have a very good reason for such rigid requirements... have you considered passing a Lambda's/Delegates/Action to the Test function instead?

Can't solve everything, but would more logically give you some of the behaviours you want (e.g. can't return, can't have class level variables, can't write code anywhere but specified).

Biggest concern with it would be captured variables... but there may be work arounds for that.

Example Code:

//I'd make a few signatures....
bool OnTest<T1, T2> (Action<ComObject, T1, T2> logic, T1 first, T2 second)
    {
        bool result = false;
        ComObject com = null;

        //no checks needed re parameters
        //Can add reflection tests here if wanted before code is run.

        try
        {
            com = new ComObject();
            //can't return
            logic(com, first,second);
        }
        finally
        {
            // Release all COM objects.
            System.Runtime.InteropServices.Marshal.ReleaseComObject(com);

            // Set all COM objects to [null].
            // The base class will take care of explicit garbage collection.
            com = null;

            //If you want, we can check each argument and if it is disposable dispose.
            if (first is IDisposable  && first != null) ((IDisposable) first).Dispose();
            ...
        }

        return (result);  //can't be changed
    }

No idea if this'll work, but it's just a thought. Oh, and as a thought it's not thorough or tested - I'd expect you to develop it drastically.

NPSF3000
  • 2,421
  • 15
  • 20
  • Thanks. You may have misunderstood the scenario unless I'm missing something. The function skeleton is generated for lots of classes and individual developers then fill them in. As an example, `[ComObject com = null;]` is not part of the template. So I'm not sure how your approach fits here. – Raheel Khan May 13 '13 at 17:34
  • "So I'm not sure how your approach fits here." Instead of trying to enforce the pattern by insisting people follow arbitrary rules, you can use inbuilt language features that'll enforce those restrictions for you. The Com Object can either be declared in the logic lambda, created in an arg creation func, passed in as an argument etc. – NPSF3000 May 13 '13 at 23:19
  • Hmmm... Could you guide me on how to use these language features to enforce such rules. – Raheel Khan May 13 '13 at 23:32