I.M. Testy

Treatises on the practice of software testing

Archive for March, 2010

Complex != Better

with one comment

I know all about over-engineering. I previously wrote about a barn my father designed and built using telephone poles and oak planks and pallets for the stall walls. From a structural perspective this barn would have stood virtually anything mother nature could have thrown at it. And if someday people wanted to tear down that barn they would surely curse the builders because the job would be much harder then they expect.

I sometimes find code that is way over-engineered. In some cases it may be necessary for making the code more robust. But, over-engineered code may also result from ignorance of more efficient algorithms or design patterns. And, sometimes overly complex algorithms are a result of developers trying to craft something that obfuscates the simplicity of the solution and fool others into believing the complexity of the solution is somehow better than a more simple solution.

I sometimes see this in test code. I am a firm believer in robust, ‘bullet-proof’ test code because each time an automated test case throws a false positive or ‘breaks’ the team loses confidence in the automation project, and it takes our time to ‘massage’ the test back to health. But, there seems to be 2 extremes in test automation. Simplistic prescriptive scripted tests with a bunch of hard-coded ‘test-data,’ or overly complex test code that is virtually undecipherable by anyone other than the original developer that renders any downstream maintenance virtually impossible. I have seen many instances where complete libraries of automation was scrapped and re-developed simply because it was easier to re-write the code rather than trying to wade through the quagmire of complexity.

In a recent example, some of my students submitted their test automation projects with a library that contained a method to generate a random string. When I first looked at their method for generating a random string I was a bit perplexed. Besides the fact that the method only produced a grand total of 26 upper case characters, the code was much more complicated than necessary. Despite having showed them how to use the Babel random string generator they choose to make their own, so I asked them how they came up with this solution and they said “they searched on the Internet. ” Others in the class indicated they also used the same method in their projects. So, I when I got home I did a quick search and found the code sample.

   1: private static string RandomString(int size)

   2: {

   3:   StringBuilder builder = new StringBuilder();

   4:   Random random = new Random();

   5:   

   6:   char ch;

   7:   for (int i = 0; i < size; i++)

   8:   {

   9:     ch = Convert.ToChar(

  10:       Convert.ToInt32(Math.Floor(26 * random.NextDouble() + 65)));

  11:     builder.Append(ch);

  12:   }

  13:  

  14:   return builder.ToString();

  15: }

OK…for a moment let’s overlook the fact that if we make 2 consecutive calls to this method we will get the same identical random string of characters (which the author of this code also discovered), and let’s assume that the range of characters is limited to upper case A through Z, and let’s also ignore the fact that we are not seeding our Random generator for reproducibility of the randomly generated output from this method.

Let’s focus on the statement in lines 9 and 10. Why in the world would we generate a number between 0.0 and 1.0, multiply it by 26 and add 65, and then use Math.Floor to return the largest integer less then or equal to the resultant double, then convert that double to a type Int32, and then convert the Int32 to a type char? Now I ask you, can we make this any more complicated?

Now, I am not critiquing the style of the code or its inherent limitations, but this solution is an example of over-engineering. As I have said before, complexity cultivates chaos. At first I thought maybe this approach might give me a better distribution of characters, but when I tested this hypothesis it simply didn’t pan out. So, the way the random character is generated is simply too complex. An easier, more readable, and effective alternative might be to simply generate a random integer within the allowable range and cast that int to a type char as illustrated below in line 10. (Yes, I realize the limitations with this approach if trying to generate surrogate pair characters above U+FFFF.)

   1: private const int minCharacter = 65;

   2: private const int maxCharacter = 91;

   3: private static string SimpleRandomString(int size)

   4: {

   5:   StringBuilder sb = new StringBuilder();

   6:   System.Threading.Thread.Sleep(1);

   7:   Random r = new Random();

   8:   for (int i = 0; i < size; i++)

   9:   {

  10:     sb.Append((char)r.Next(minCharacter, maxCharacter + 1));

  11:   }

  12:  

  13:   return sb.ToString();

  14: }

BTW…the Sleep() in line 6 is a easy solution to preventing identical return values for consecutive calls. But, a more effective solution would be to pass in a seed value as a parameter to the method and use that to seed the new Random generator as illustrated below. Different seeds not only guarantee randomness in the resultant string, but also allow for repeatability if necessary as long as the seed value is preserved.

   1: private static string SimpleRandomString(int size, int seed)

   2: {

   3:   StringBuilder sb = new StringBuilder();

   4:   Random r = new Random(seed);

   5:   ...

   6: }

But, I digress. This post isn’t about random generation or style…it’s about complexity. Overly complex code tends to harbor errors that might go undetected (at least initially). Also, maintenance of complex code not only becomes more problematic, it often leads to costly re-writes down the road.

If we consider that greater complexity may increase the likelihood of error, then we don’t want our development partners to unnecessarily over-engineered algorithms. Also, overly complex solutions often require overly complex testing. This not only leads to increased testing costs, but it also increases the probability of an important test being missed or overlooked. Think testability!

Finally, with regard to test automation we should consider that our automated tests might be reused by other teams, and they certainly will be used during maintenance or sustained engineering efforts well after the product has released. And, in some cases, the people maintaining the product might not be the same people who shipped the product. Well-written automated tests are not just reviewable by someone other than the author of the code, but they should also be easily maintainable. Otherwise, we might end up paying double for that automated test case. Ouch!

Written by Bj Rollison

March 31st, 2010 at 8:43 pm

Boundary Bugs…like shooting fish in a barrel

with 6 comments

If there is a bug at a boundary that doesn’t lead to an unhandled exception or security exploit should we care?

Perhaps an even more important question is why do we find so many boundary type bugs via exploratory testing when they can and should be caught earlier? Why don’t we find these types of bugs in our unit testing? Why don’t we find these types of bugs by more systematically testing the software? Maybe we do find them, and those who make the decisions to fix these types of bugs just don’t care if they are fixed because there is no severe negative impact to the user. Maybe someone just wants to give me fodder for my blog!

This week I wanted to compare the range of allowable font sizes for a simulation program I developed as an example for a magazine article that I am working on. I knew that Office applications allow a font size within the range of 1 – 1638. I thought that range might be too large for my purposes, and since I knew that Windows Notepad included a font dialog I decided to check the allowable range of font sizes in Notepad.

The first thing I discovered was that the combobox control allows up to 5 characters! Really? Someone decided it is a good idea to allow users to enter 5 characters? notepad 1

OK, I’ll play along. Maybe if I put in a size of 99999 and press the OK button on the dialog I will get an error message, or at least Notepad defaults to the last ‘valid’ selected font size. That might seem reasonable. But is that what happens? NO! Instead of doing something reasonable (e.g. error message, default font size) the font changes to a size of 1 (yes that is a font size 1 in the upper left corner in the image below).

Notepad 2

I am sure that defaulting to a font size of 1 makes sense when the allowable size value overflows! Really…someone thought that was a good idea? Now I wanted to see what magical boundary value the developer decided was an acceptable font size. Since the combobox size property allowed 5 characters I immediately tried 65535. No, that also resulted in the overflow and displayed the text in a font size of 1. Next I tried 32767. Wait…32767 didn’t display the string in Notepad’s edit control at a font size of 1. Now, I am thinking the developer is using a data type of signed short for the font size variable. So, I enter 32768 expecting the value to overflow and display my string as a size 1 font again. But, no…that doesn’t happen.

Now, when I am design boundary tests I generally rely on 2 heuristics for identifying boundary values for input or output parameters.

  1. Values at the extreme edges of a physical range of values
  2. Values at the edges of equivalence partitions of physical values

So, in these situations I ask myself “What sort of demented developer debauchery have I now found myself?” I can’t think of any other obvious edge values that might apply, so out of curiosity I quickly narrow down the magical value to 39321. I then ask myself, “OK…even if there were a display capable of rendering or a printer capable of printing a font of this size, what is so unique about 39321?” In hexadecimal it is 0×9999, and in binary it is 1001100110011001. OK…nothing obviously special here, but I am certain the implementation details are much more complex then a simple range of values and at this point I really don’t care because this bug just doesn’t make sense.

Maybe it’s not supposed to make sense! Maybe nobody really cares about these types of bugs!

(BTW…somebody please take the Thesaurus away from the developer…’Oblique?’ Are you serious…why not just be consistent and use the word ‘Italic?’)

Written by Bj Rollison

March 24th, 2010 at 11:58 pm

Meaningful Measures

with 2 comments

I arrived in Switzerland on Monday morning and met with our team here in Zurich who work on the communication server. Tuesday I presented a tutorial on advanced combinatorial testing and delivered a keynote address at Swiss Testing Days on Wednesday. Unfortunately, I really didn’t get to spend a lot of time exploring the city, but it was great to catch up with my long time friend James Whittaker. James and I also gave brief presentations at an executive dinner the night prior to the conference. It was also really nice to meet new friends from SwissQ who put together Swiss Testing Days. This was my first time to present at this conference and I was greatly impressed. More than 750 people attended the conference! It was quite an event and I hope to return next year.

At the executive dinner and during my keynote I discussed various challenges in software engineering that directly impact testers. One of those challenges we need to get our heads wrapped around is software measures. By software measures I am referring to objects in software engineering mapped to various scales in the mathematical world. Although we sometimes also use biased qualitative measures, such as “too slow,” if we are to be taken with any degree of credibility we have to define what to slow is and set a reasonable goal for ‘acceptable’ based on customer values.

As testers we expend a lot of cycles collecting buckets full of metrics. We spend time producing fancy charts, and spend countless hours ‘looking’ at the data as if it were some type of oracle that would speak to us and tell us what we wanted to know. In the best case we convince ourselves that the numbers are telling us what we want the numbers to tell us. In the worse case the decision makers do not even consider the measures, or we don’t analyze the data in an attempt to identify ways to improve some of our engineering processes and practices. In the end, all the fancy charts are taken off the walls only to be shredded and we start over.

We often get caught up in tracking mostly useless data such as bug count and code coverage. What in the world does bug count or code coverage tell us (or the decision makers) about quality? Nothing; absolutely nothing! Some people want to believe that finding a lot of bugs or have high levels of code coverage means better quality, but that is sort of like believing that you’ll find a pot of gold and a leprechaun at the end of every rainbow. So, why do we measure bug counts and code coverage? Simple…because they are easy to measure!

Good metrics are hard to define mostly because we don’t always have clear goals, or we use a scatter-gun approach to setting a bunch of disparate wishful goals (goals that we hope we can achieve, but nobody is accountable if we don’t). I personally advocate the Goal/Question/Metric paradigm by Victor Basili. But, the biggest problem I have in using this approach is in establishing meaningful goals! People are generally good with coming up with superfluous objectives such as 100% automation or 80% code coverage. But, when you ask those people why they want 100% automation or 80% code coverage they retort only with a bunch of hand-waving and philosophical arguments. It seems we sometimes have difficulty expressing the ‘why’ of setting certain goals. Of course the answer in most cases is to ‘get better’ or ‘improve’ something! But, why? What is the business value?

Once we establish clear goals the next step is to understand the variables that we can manipulate to help us achieve those goals. Then we must decide on which ones we want to change that we think will have the biggest bang for the buck. Finally, we figure out which measures will let us know whether we are progressing towards our goal. (This usually isn’t a single point of measurement.)

At one time I naively believed that there was a core set of metrics that all teams should be collecting all the time that we could put into a ‘dashboard’ and compare across teams. In retrospect that was really a bone-headed notion. Identifying these measures is not easy, and there is no cookie-cutter approach. Each project team needs to decide on their specific goals that may increase customer value or impact business costs. Testers should ask themselves, “why are we measuring this?” “What actions will be taken as a result of these measures?” And, “if there is no actionable objective associated with this measure, then why am I spending time measuring this?”

At times is seems we are locked in a vicious cycle of relearning things via tribal knowledge, and we make decisions based mostly on ‘gut-feel’ and emotion. We collect a bunch of measures and display them similar to how the ancient Chinese used the mystical ‘dragon bones’ as oracles. But, if we are interested in being able to articulate business impact (either positive or negative) in a professional manner then we must be able to find ways to measure the things that are really important and actionable, and spend less time collecting numbers for wall decorations. At the end of the day someone is going to ask, “How do we know?” And trust me on this…really great managers will eat you alive if you answer with “well, we think…” or “we feel…” or try to evaluate success on some other subjective measure.

Written by Bj Rollison

March 21st, 2010 at 2:22 am

Do I Really Need To Automate This Test?

with 4 comments

For the past 2 weeks my students in my automation course at University of Washington have been tasked with designing automated test cases through the GUI for a shareware program. In my opinion, GUI automation is the least effective approach for testing the functional or business logic of a program (assuming a well designed architecture where the business logic code is separate from the form (GUI) and the event hander (GUI object behavior) code). However, if a tester doesn’t have access to the underlying APIs used in the application under test (AUT) and is given just a compiled application (‘GUI application’) to test then functional testing through the GUI may be the only alternative.

GUI automation can be effective for some types of behavioral and non-functional tests such as performance and stress testing. It can also be useful in checking for layout issues such as control alignment, and clipping or truncation of controls on a dialog much more effectively than compared to the human eye.

However, there are some behavioral tests that are more efficient to perform manually by ‘me’ the tester. For example, end-2-end user scenarios are designed to simulate a customer completing some task involving multiple features and system interactions. Sure, we could automate these types of tests and I can even design my automated test to simulate emotions such as frustration by timing out if an event takes ‘too long’ or anger because of ‘too many’ pop-ups. (Of course, I’d have to specify ‘too long’ and ‘too many.’)  But, in my opinion we shouldn’t automate things like end-2-end scenarios because automation is poor at emulating a real person. I write automated tests to provide value to ‘me’ the tester; to free up my time to test the things that are better tested by ‘me.’

There are other types of GUI test cases that I need to execute, but shouldn’t be automated. One student wanted to automate a test that clicked the buttons on the toolbar but was having difficulty accessing the toolbar buttons on a native code application using C#. Now, in my opinion, spending time to automate a test case to ‘validate’ the toolbar buttons makes about as much sense as automating a test to validate the tab order of a dialog or checking duplicate access key mnemonics. The question is not how can we can automate ‘test cases’ for tab-order, key mnemonics, or the toolbar buttons; the question is should we?

First, I explained to the student that the difficulty was due to the fact that toolbar buttons are not the same as common button controls (e.g. OK or Cancel buttons). Toolbar “buttons” are actually bitmap images that sit on a toolbar control and just look and act similar to small buttons. Next, I asked the student, “Since I know you are not testing the toolbar control itself, what is the purpose of this test; what exactly are you testing?” He replied, “To make sure it works.” Again I asked, “What exactly are you testing, what specifically are you making sure works?” Finally he replied, “To make sure the toolbar button triggers the appropriate event handler.” I thought to myself, “Great! They are starting to think about how this stuff works below the covers.” The questioning continued, “Are there other ways to trigger the same events? The student replied, “Yes, there are menu items.” In fact, most toolbar buttons are essentially shortcuts so users don’t have to navigate dropdown menus. The example program below illustrates how toolbar buttons provide a visual cue to the user, but end up calling the same event handler as the menu item.

menu items toolbar buttons

So I asked, “Since there is a menu item that calls the same apparent event as the toolbar button, do you think there are two separate event handlers for the same behavior; one for the menu item click event and another for the toolbar button click event, or do you think the menu item click event and the toolbar button click event call the same event handler?”

The answer here could depend on whether or not we are dealing with competent developers. For example, as we build out the event handlers for the UI element I guess we could create 4 separate events (2 that do the same thing) as illustrated below.

4 event handlers

Competent developers would of course realize we only need 1 event handler for the ‘click’ events for the align right menu item and toolbar button, and 1 event handler for the align left menu item and toolbar button since there is no behavioral difference between clicking the menu item or clicking the toolbar button in this situation. So, our developer refactors the code to have 1 event handler for each specific behavior similar to:

2 event handlers

and then updates the appropriate UI element Click event statements in the form designer code to call the appropriate event handler for the menu items and as illustrated below for the toolbar buttons.

update click events

But, I still wasn’t completely convinced of the purpose of his test case. So, I asked, “Are you testing the event handler, or are you testing to make sure the toolbar button “click” event calls the appropriate event handler?” To which he responded, “To make sure the toolbar button ‘click’ event calls the ‘correct’ event handler.”

“OK,” I said in a pondering sort of way, “Let me get this right. You are going to spend some amount of time to automate a test that will validate whether or not each toolbar button click event calls the appropriate event handler.” Then I proceeded to click each toolbar button on the application under test to trigger the expected behavior. The few buttons only took a matter of a few seconds. Then I looked at him and asked, “Are you sure you want to spend time automating a test to do what I just did in a few seconds? “Are you sure you want to automate a test that has an extremely low probability of changing during the product development lifecycle? “Are you sure you want to automate a test that will probably get a lot of “face time” by testers, developers, beta testers, and others on the team? “Are you sure you want to automate a test case that you will likely spend even more time massaging and maintaining over the product shelf-life? “Or, do you think it might be a more efficient use of your time to take a few seconds and test this once per sprint cycle or milestone and let dog-fooding, beta testing, self-hosting, etc. help in ‘testing’ the behavior of those toolbar buttons?’”

I suspect this is a case of “well, this is a test that I need to test at least once, so we should automate it if we can.” Certainly we need to test toolbar buttons to make sure they trigger the appropriate event handler; once, maybe once per milestone or sprint cycle. But, do I really need to automate this test? In a similar case, one tester at Microsoft said to me, “we have to constantly retest this in sustained engineering and if we don’t automate this test then we will have to hire testers to test it manually.

Besides the faulty logic of retesting unchanged code or code that is not impacted by other changes repeatedly (and we have lots of tools to show us code churn and dependencies between modules that might be affected by churn) and beside the foolish notion that automation will replace testers, I will say that I would rather have a tester spend a few seconds each cycle testing whether a toolbar button event calls the appropriate event handler rather than have a tester spend hours/days/weeks baby-sitting and massaging temperamental GUI test code.

This is not to say that all GUI automation is finicky. And this is not to say that we shouldn’t consider automating our test cases. But, we shouldn’t automate for the sake of trying to automate all our test cases, and we certainly shouldn’t automate mindlessly simple tests; especially automated tests that might require more of my time in the long run or that have little value (virtually zero probability of  new information) to the overall testing effort when executed. (Just because a test is automated doesn’t mean it’s free!)

Before we develop an automated test we should really think about the test design from a “what am I REALLY testing here” perspective and then ask, “Does this really make sense to have a separate automated test case, or is this behavior or functionality being covered by other tests (manual and/or automated) sufficiently?”

Written by Bj Rollison

March 8th, 2010 at 7:00 am

Programmatically Detecting The Operating System Version (Part II)

without comments

Time is a commodity in short supply! It has been more than 2 weeks since my last post. I have not been sitting idle, but really haven’t had a lot of free time to write. In preparation for my up-coming trip to Zurich, Switzerland to give a workshop and keynote at Swiss Testing Day I was interviewed by Marco van der Spek for TESTNIEUWS.NL. The interview provides a bit more background about me and some of my perspectives of Microsoft and software testing.

Also sucking up some of my time over the past couple of weeks were ‘reorgs’ at work. Change at Microsoft is a constant. Most people get used to it after awhile; others still freak out even with the slightest change. For me it mostly means shifting some priorities based on the new General Manager’s strategic vision, acclimating to a new manager and letting him know what I am working on, and generally making sure the day to day business on our team keeps moving forward during the transition.

Just like life at Microsoft and technology in general the Windows operating system continuously goes through changes. New versions, new service packs, Ultimate, Home, Server versions, etc. Sometimes it is hard to keep up with all the changes. And from a test automation perspective it is sometimes important to know which operating system version the test is running on. In some cases control flow in the automated test case may need to branch in order for the test to execute on different operating system versions. Branching in an automated test based on the operating system version eliminates the need to write separate test cases for variances in operating system versions.

And, certainly if our test matrix includes multiple product versions (Home, Ultimate, Professional, etc) and our automated test exposes a bug then we certainly want to collect information about the operating system version the test was running on. This is especially important if the automated test fails on one machine, but passes on another. Sometimes the cause of the failure may be a slight difference in the machine configuration or the operating system version.

Almost 2 years ago I described a way to get the operating system version information using the System.PlatformID enumeration and the System.Environment.OSVersion property and OperatingSystem class members in this blog post. But, I also mention some limitations such as detecting the specific edition or product type of a Windows Version. Another limitation is the difficulty in detecting whether the operating system version is Windows 7 or Windows 2008 Server R2.

I also mentioned that in order to identify a particular version  and/or edition of Windows we need to invoke the Win32 GetVersionEx() function. If a test is dependent on a specific edition of Windows Vista or Windows Server 2008 then we can invoke the Win32 GetProductInfo() function. But, to use these Win32 APIs we need to use platform invocation services (P/Invoke) to marshal native code into our managed test code.

The code snippet below illustrates the required Win32 function marshaled using the DLLImport attribute in C#, constant values, wrapper methods to get common operating system information, and public properties to get the operating system version, any installed service pack information, and the operating system edition for Windows Vista, Windows Server 2008, and Windows 7.

   1: // <copyright file = VersionInfo.cs" company = "Testing Mentor">

   2: // Copyright © 2010 All Rights Reserved. Test developers can simply copy and

   3: // paste the code into their code, but may not reproduce or publish the code

   4: // snippets on any web site, online service, or distribute as source on any

   5: // media without express written permission. </copyright>

   6:  

   7: namespace TestingMentor.Snippet.OperatingSystemVersionInfo

   8: {

   9:   using System;

  10:   using System.Runtime.InteropServices;

  11:  

  12:   public class WindowsVersionInfo

  13:   {

  14:     public string GetOSVersion

  15:     {

  16:       get { return this.GetOSVersionInfo(); }

  17:     }

  18:  

  19:     public string GetServicePack

  20:     {

  21:       get { return this.GetServicePackInfo(); }

  22:     }

  23:  

  24:     public string GetProductType

  25:     {

  26:       get { return this.GetProductTypeInfo(); }

  27:     }

  28:  

  29:     private string GetOSVersionInfo()

  30:     {

  31:       string version = "Unsupported Version";

  32:  

  33:       NativeMethods.OSVersionInfoEx osvi = new NativeMethods.OSVersionInfoEx();

  34:       osvi.VersionInfoSize = 

  35:         Marshal.SizeOf(typeof(NativeMethods.OSVersionInfoEx));

  36:       NativeMethods.GetVersionEx(ref osvi);

  37:  

  38:       if (OsviConstant.SupportedPlatform == osvi.PlatformId &&

  39:         osvi.MajorVersion > 4)

  40:       {

  41:         if (osvi.MajorVersion == (int)OsviConstant.MajorVersion.NT5 &&

  42:           osvi.MinorVersion == (int)OsviConstant.MinorVersion.Windows2000)

  43:         {

  44:           version = "Windows 2000";

  45:         }

  46:  

  47:         if (osvi.MajorVersion == (int)OsviConstant.MajorVersion.NT5 &&

  48:           osvi.MinorVersion == (int)OsviConstant.MinorVersion.WindowsXP)

  49:         {

  50:           version = "Windows XP";

  51:         }

  52:  

  53:         if (osvi.MajorVersion == (int)OsviConstant.MajorVersion.NT5 &&

  54:           osvi.MinorVersion == (int)OsviConstant.MinorVersion.WindowsServer2003)

  55:         {

  56:           if (osvi.ProductType == (byte)OsviConstant.WorkStation)

  57:           {

  58:             version = "Windows XP Professional x64";

  59:           }

  60:           else

  61:           {

  62:             version = "Windows Server 2003";

  63:             if (NativeMethods.GetSystemMetrics(OsviConstant.ServerR2) != 0)

  64:             {

  65:               version += " R2";

  66:             }

  67:           }

  68:         }

  69:  

  70:         if (osvi.MajorVersion == (int)OsviConstant.MajorVersion.NT6 &&

  71:           osvi.MinorVersion == (int)OsviConstant.MinorVersion.WindowsVista)

  72:         {

  73:           if (osvi.ProductType ==

  74:             (byte)OsviConstant.WorkStation)

  75:           {

  76:             version = "Windows Vista";

  77:           }

  78:           else

  79:           {

  80:             version = "Windows Server 2008";

  81:           }

  82:         }

  83:  

  84:         if (osvi.MajorVersion == (int)OsviConstant.MajorVersion.NT6 &&

  85:           osvi.MinorVersion == (int)OsviConstant.MinorVersion.Windows7)

  86:         {

  87:           if (osvi.ProductType == (byte)OsviConstant.WorkStation)

  88:           {

  89:             version = "Windows 7";

  90:           }

  91:           else

  92:           {

  93:             version = "Windows Server 2008 R2";

  94:           }

  95:         }

  96:       }

  97:  

  98:       return version;

  99:     }

 100:  

 101:     private string GetServicePackInfo()

 102:     {

 103:       NativeMethods.OSVersionInfoEx versionInfo = new NativeMethods.OSVersionInfoEx();

 104:       versionInfo.VersionInfoSize = Marshal.SizeOf(typeof(NativeMethods.OSVersionInfoEx));

 105:       NativeMethods.GetVersionEx(ref versionInfo);

 106:       return versionInfo.CSDVersion; 

 107:     }

 108:  

 109:     private string GetProductTypeInfo()

 110:     {

 111:       string product = String.Empty;

 112:  

 113:       NativeMethods.OSVersionInfoEx osvi = new NativeMethods.OSVersionInfoEx();

 114:       osvi.VersionInfoSize =

 115:         Marshal.SizeOf(typeof(NativeMethods.OSVersionInfoEx));

 116:       NativeMethods.GetVersionEx(ref osvi);

 117:  

 118:       if (osvi.MajorVersion > 5)

 119:       {

 120:         uint productType = 0;

 121:  

 122:         NativeMethods.GetProductInfo(

 123:           osvi.MajorVersion,

 124:           osvi.MinorVersion,

 125:           osvi.ServicePackMajor,

 126:           osvi.ServicePackMinor,

 127:           ref productType);

 128:  

 129:         switch (productType)

 130:         {

 131:           case (uint)OsviConstant.ProductInfo.Business:

 132:             product = "Business Edition";

 133:             break;

 134:           case (uint)OsviConstant.ProductInfo.BusinessN:

 135:             product = "Business N Edition";

 136:             break;

 137:           case (uint)OsviConstant.ProductInfo.ClusterServer:

 138:             product = "HPC Edition";

 139:             break;

 140:           case (uint)OsviConstant.ProductInfo.DatacenterServer:

 141:             product = "Server Datacenter (Full)";

 142:             break;

 143:           case (uint)OsviConstant.ProductInfo.DatacenterServerCore:

 144:             product = "Server Datacenter (Core)";

 145:             break;

 146:           case (uint)OsviConstant.ProductInfo.DataCenterServerCoreV:

 147:             product = "Server Datacenter without Hyper-V (Core)";

 148:             break;

 149:           case (uint)OsviConstant.ProductInfo.DataCenterServerV:

 150:             product = "Server Datacenter without Hyper-V (Full)";

 151:             break;

 152:           case (uint)OsviConstant.ProductInfo.Enterprise:

 153:             product = "Enterprise Edition";

 154:             break;

 155:           case (uint)OsviConstant.ProductInfo.EnterpriseE:

 156:             product = "Enterprise E Edition";

 157:             break;

 158:           case (uint)OsviConstant.ProductInfo.EnterpriseN:

 159:             product = "Enterprise N Edition";

 160:             break;

 161:           case (uint)OsviConstant.ProductInfo.EnterpriseServer:

 162:             product = "Server Enterprise (Full)";

 163:             break;

 164:           case (uint)OsviConstant.ProductInfo.EnterpriseServerCore:

 165:             product = "Server Enterprise (Core)";

 166:             break;

 167:           case (uint)OsviConstant.ProductInfo.EnterpriseServerCoreV:

 168:             product = "Server Enterprise without Hyper-V (Core)";

 169:             break;

 170:           case (uint)OsviConstant.ProductInfo.EnterpriseServerIA64:

 171:             product = "Server Enterprise for Itanium-based Systems";

 172:             break;

 173:           case (uint)OsviConstant.ProductInfo.EnterpriseServerV:

 174:             product = "Server Enterprise without Hyper-V (Full)";

 175:             break;

 176:           case (uint)OsviConstant.ProductInfo.HomeBasic:

 177:             product = "Home Basic Edition";

 178:             break;

 179:           case (uint)OsviConstant.ProductInfo.HomeBasicE:

 180:             product = "Home Basic E Edition";

 181:             break;

 182:           case (uint)OsviConstant.ProductInfo.HomeBasicN:

 183:             product = "Home Basic N Edition";

 184:             break;

 185:           case (uint)OsviConstant.ProductInfo.HomePremium:

 186:             product = "Home Premium Edition";

 187:             break;

 188:           case (uint)OsviConstant.ProductInfo.HomePremiumE:

 189:             product = "Home Premium E Edition";

 190:             break;

 191:           case (uint)OsviConstant.ProductInfo.HomePremiumN:

 192:             product = "Home Premium N Edition";

 193:             break;

 194:           case (uint)OsviConstant.ProductInfo.HomeServer:

 195:             product = "Home Server Edition";

 196:             break;

 197:           case (uint)OsviConstant.ProductInfo.HyperV:

 198:             product = "Microsoft Hyper-V Server";

 199:             break;

 200:           case (uint)OsviConstant.ProductInfo.MediumBusinessServerManagement:

 201:             product = "Windows Essential Business Server Management Server";

 202:             break;

 203:           case (uint)OsviConstant.ProductInfo.MediumBusinessServerMessaging:

 204:             product = "Windows Essential Business Server Messaging Server";

 205:             break;

 206:           case (uint)OsviConstant.ProductInfo.MediumBusinessServerSecurity:

 207:             product = "Windows Essential Business Server Security Server";

 208:             break;

 209:           case (uint)OsviConstant.ProductInfo.Professional:

 210:             product = "Professional Edition";

 211:             break;

 212:           case (uint)OsviConstant.ProductInfo.ProfessionalE:

 213:             product = "Professional E Edition";

 214:             break;

 215:           case (uint)OsviConstant.ProductInfo.ProfessionalN:

 216:             product = "Professional N Edition";

 217:             break;

 218:           case (uint)OsviConstant.ProductInfo.ServerForSmallBusiness:

 219:             product = "Windows Server 2008 for Windows Essential Server Solutions";

 220:             break;

 221:           case (uint)OsviConstant.ProductInfo.ServerForSmallBusinessV:

 222:             product = "Windows Server 2008 without Hyper-V for Windows Essential Server Solutions";

 223:             break;

 224:           case (uint)OsviConstant.ProductInfo.ServerFoundation:

 225:             product = "Server Foundation";

 226:             break;

 227:           case (uint)OsviConstant.ProductInfo.SmallBusinessServer:

 228:             product = "Windows Small Business Server";

 229:             break;

 230:           case (uint)OsviConstant.ProductInfo.SmallBusinessServerPremium:

 231:             product = "Windows Small Busines Server Premium";

 232:             break;

 233:           case (uint)OsviConstant.ProductInfo.StandardServer:

 234:             product = "Server Standard (Full)";

 235:             break;

 236:           case (uint)OsviConstant.ProductInfo.StandardServerCore:

 237:             product = "Server Standard (Core)";

 238:             break;

 239:           case (uint)OsviConstant.ProductInfo.StandardServerCoreV:

 240:             product = "Server Standard without Hyper-V (Core)";

 241:             break;

 242:           case (uint)OsviConstant.ProductInfo.StandardServerV:

 243:             product = "Server Standard without Hyper-V (Full)";

 244:             break;

 245:           case (uint)OsviConstant.ProductInfo.Starter:

 246:             product = "Starter Edition";

 247:             break;

 248:           case (uint)OsviConstant.ProductInfo.StarterE:

 249:             product = "Starter E Edition";

 250:             break;

 251:           case (uint)OsviConstant.ProductInfo.StarterN:

 252:             product = "Starter N Edition";

 253:             break;

 254:           case (uint)OsviConstant.ProductInfo.StorageEnterpriseServer:

 255:             product = "Storage Server Enterprise";

 256:             break;

 257:           case (uint)OsviConstant.ProductInfo.StorageExpressServer:

 258:             product = "Storage Server Express";

 259:             break;

 260:           case (uint)OsviConstant.ProductInfo.StorageStandardServer:

 261:             product = "Storage Server Standard";

 262:             break;

 263:           case (uint)OsviConstant.ProductInfo.StorageWorkgroupServer:

 264:             product = "Storage Server Workgroup";

 265:             break;

 266:           case (uint)OsviConstant.ProductInfo.Ultimate:

 267:             product = "Ultimate Edition";

 268:             break;

 269:           case (uint)OsviConstant.ProductInfo.UltimateE:

 270:             product = "Ultimate E Edition";

 271:             break;

 272:           case (uint)OsviConstant.ProductInfo.UltimateN:

 273:             product = "Ulitmate N Edition";

 274:             break;

 275:           case (uint)OsviConstant.ProductInfo.Undefined:

 276:             product = "Unknown Product";

 277:             break;

 278:           case (uint)OsviConstant.ProductInfo.Unlicensed:

 279:             product = "Unlicensed or Expired";

 280:             break;

 281:           case (uint)OsviConstant.ProductInfo.WebServer:

 282:             product = "Web Server (Full)";

 283:             break;

 284:           case (uint)OsviConstant.ProductInfo.WebServerCore:

 285:             product = "Web Server (Core)";

 286:             break;

 287:         }

 288:       }

 289:  

 290:       return product;

 291:     }

 292:   }

 293:  

 294: // ****************************************************************************

 295: // NEW CLASS - SHOULD BE PLACED IN SEPARATE FILE

 296: // ****************************************************************************

 297:   

 298:   internal class OsviConstant

 299:   {

 300:     internal const int SupportedPlatform = 2;

 301:     internal const int ServerR2 = 89;

 302:     internal const int WorkStation = 0x00000001;

 303:  

 304:     private OsviConstant()

 305:     {

 306:     }

 307:  

 308:     internal enum MajorVersion

 309:     {

 310:       NT5 = 5,

 311:       NT6 = 6

 312:     }

 313:  

 314:     internal enum MinorVersion

 315:     {

 316:       Windows2000 = 0,

 317:       WindowsXP = 1,

 318:       WindowsServer2003 = 2,

 319:       WindowsVista = 0,

 320:       Windows7 = 1

 321:     }

 322:  

 323:     internal enum ProductInfo : uint

 324:     {

 325:       Business = 0x00000006,

 326:       BusinessN = 0x00000010,

 327:       ClusterServer = 0x00000012,

 328:       DatacenterServer = 0x00000008,

 329:       DatacenterServerCore = 0x0000000C,

 330:       DataCenterServerCoreV = 0x00000027,

 331:       DataCenterServerV = 0x00000025,

 332:       Enterprise = 0x00000004,

 333:       EnterpriseE = 0x00000046,

 334:       EnterpriseN = 0x0000001B,

 335:       EnterpriseServer = 0x0000000A,

 336:       EnterpriseServerCore = 0x0000000E,

 337:       EnterpriseServerCoreV = 0x00000029,

 338:       EnterpriseServerIA64 = 0x0000000F,

 339:       EnterpriseServerV = 0x00000026,

 340:       HomeBasic = 0x00000002,

 341:       HomeBasicE = 0x00000043,

 342:       HomeBasicN = 0x00000005,

 343:       HomePremium = 0x00000003,

 344:       HomePremiumE = 0x00000044,

 345:       HomePremiumN = 0x0000001A,

 346:       HyperV = 0x0000002A,

 347:       MediumBusinessServerManagement = 0x0000001E,

 348:       MediumBusinessServerSecurity = 0x0000001F,

 349:       MediumBusinessServerMessaging = 0x00000020,

 350:       Professional = 0x00000030,

 351:       ProfessionalE = 0x00000045,

 352:       ProfessionalN = 0x00000031,

 353:       ServerForSmallBusiness = 0x00000018,

 354:       ServerForSmallBusinessV = 0x00000023,

 355:       ServerFoundation = 0x00000021,

 356:       SmallBusinessServer = 0x00000009,

 357:       StandardServer = 0x00000007,

 358:       StandardServerCore = 0x0000000D,

 359:       StandardServerCoreV = 0x00000028,

 360:       StandardServerV = 0x00000024,

 361:       Starter = 0x0000000B,

 362:       StarterE = 0x00000042,

 363:       StarterN = 0x0000002F,

 364:       StorageEnterpriseServer = 0x00000017,

 365:       StorageExpressServer = 0x00000014,

 366:       StorageStandardServer = 0x00000015,

 367:       StorageWorkgroupServer = 0x00000016,

 368:       Undefined = 0x00000000,

 369:       Ultimate = 0x00000001,

 370:       UltimateE = 0x00000047,

 371:       UltimateN = 0x0000001C,

 372:       WebServer = 0x00000011,

 373:       WebServerCore = 0x0000001D,

 374:       Unlicensed = 0xABCDABCD,

 375:       HomeServer = 0x00000013,

 376:       SmallBusinessServerPremium = 0x00000019,

 377:     }

 378:   }

 379:  

 380: // ****************************************************************************

 381: // NEW CLASS - SHOULD BE PLACED IN SEPARATE FILE

 382: // ****************************************************************************

 383:  

 384:   internal class NativeMethods

 385:   {

 386:     private NativeMethods()

 387:     {

 388:     }

 389:  

 390:     [DllImport("kernel32")]

 391:     [return: MarshalAs(UnmanagedType.Bool)]

 392:     internal static extern bool GetVersionEx(ref OSVersionInfoEx osvi);

 393:  

 394:     [DllImport("kernel32.dll")]

 395:     [return: MarshalAs(UnmanagedType.Bool)]

 396:     internal static extern bool GetProductInfo(

 397:       int osMajorVersion,

 398:       int osMinorVersion,

 399:       int spMajorVersion,

 400:       int spMinorVersion,

 401:       ref uint type);

 402:  

 403:     [DllImport("kernel32.dll")]

 404:     internal static extern int GetSystemMetrics(

 405:       int index);

 406:  

 407:     [StructLayout(LayoutKind.Sequential)]

 408:     internal struct OSVersionInfoEx

 409:     {

 410:       public int VersionInfoSize;

 411:       public int MajorVersion;

 412:       public int MinorVersion;

 413:       public int BuildNumber;

 414:       public int PlatformId;

 415:       [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]

 416:       public string CSDVersion;

 417:       public Int16 ServicePackMajor;

 418:       public Int16 ServicePackMinor;

 419:       public Int16 SuiteMask;

 420:       public byte ProductType;

 421:       public byte Reserved;

 422:     }

 423:   }

 424: }

Now, some of my readers have indicated that these code snippets are not very useful because they can’t copy them directly and put them to use. So, to help resolve that issue I have created a new section on my web site called the Code Snippet Library. This snippet is posted there along with fully annotated (mostly FxCopy and StyleCopy compliant) file available for download or to copy for inclusion in your automated test cases, or compiled into a dynamic link library (DLL).

This example doesn’t differentiate between 32-bit and 64-bit Windows operating systems, but that is not really difficult to add, and if I get enough requests I will certainly add that into the pot. If the operating system version is no longer supported by Microsoft the GetOsVersion property will return "Unsupported Version." If no Service Packs are installed the GetServicePack property will return an empty string. If you need to detect an unsupported operating system version use the example here.

Written by Bj Rollison

March 3rd, 2010 at 11:33 am