Ureader.com  
Microsoft software help and Community
   home   |   control panel login   |   archive   |  
 
platform
active.directory
adsi
adsi.iis-admin
base
com_ole
complus_mts
component_svcs
database
directx
gdi
graphics_mm
internet.client
internet.server
internet.server.isapi-dev
localization
mapi
messaging
msi
mslayerforunicode
multimedia
networking
networking.ipv6
sdk_install
security
shell
telephony.tapi_2
telephony.tapi_3
telephony.tsp
telephony.wte
tools
ui
ui_shell
win_base_svcs
win16
  
 
date: Tue, 12 Aug 2008 04:39:48 -0700 (PDT),    group: microsoft.public.platformsdk.msi        back       


How do I execute deferred CAs programmatically (for unit testing customactions)?   
I've been working on unit-testing our C++ custom actions. In the
beginning I had a shim that simulated MSI functions, but once we got
to the complex ones (MsiExecuteQuery), it became clear that that's not
the way to go. Today I create a database on the fly and can call any
MSI function. Wrapped into CPPUNIT this makes a complete MSI testing
framework.

It looks roughly like this:

MsiOpenDatabase(temporary file, MSIDBOPEN_CREATEDIRECT); // create a
new blank database
MsiGetSummaryInformation(...) // get existing summary information
MsiSummaryInfoSetProperty(...) // set new minimal summary information
for msi to be happy (PID_REVNUMBER, PID_PAGECOUNT and PID_WORDCOUNT),
repeat for each property
MsiSummaryInfoPersist // persist the property information in the msi
MsiDatabaseCommit // commit the changes to disk
MsiOpenPackage(temporaryfilename, & handle) // reopen the database as
a runtime package

Now I have an MSIHANDLE that can be passed into MSI functions. To call
MsiDoAction, I also import the CustomAction and Binary tables that
have the declaration of my custom action and the custom action binary
respectively. Works well for immediate CAs.

The one thing I can't figure out is how to execute deferred CAs? I
tried running the whole sequence via MsiDoAction, but it always fails
in InstallInitialize with 0x80070643: Fatal error during installation.

Ideas?
date: Tue, 12 Aug 2008 04:39:48 -0700 (PDT)   author:   dB.

Re: How do I execute deferred CAs programmatically (for unit testing customactions)?   
[Please do not mail me a copy of your followup]

"dB."  spake the secret code
 thusly:

>The one thing I can't figure out is how to execute deferred CAs? I
>tried running the whole sequence via MsiDoAction, but it always fails
>in InstallInitialize with 0x80070643: Fatal error during installation.

I have been pursuing unit tests of my C++ CAs as well.  What I do is
treat the CA as a function to be called and I fake out the MSI API
with mock API functions.  Since the session MSIHANDLE is an opaque
data type anyway, it won't matter what you pass into your CA for this
handle.  It does mean that you have to provide a level of indirection
between your CA and the MSI API so that your unit test can intercept
things at the level of indirection.  Here's one way of doing it:

class MsiAPI
{
public:
    virtual ~MsiAPI() { }

    UINT MsiGetProperty(MSIHANDLE session,
        LPCTSTR name, LPTSTR value, DWORD *valueSize) = 0;
};

class ProductionAPI : public MsiAPI
{
public:
    virtual ~ProductionAPI() { }

    UINT MsiGetProperty(MSIHANDLE session,
        LPCTSTR name, LPTSTR value, DWORD *valueSize)
    { return ::MsiGetProperty(session, name, value, valueSize); }
};

typedef std::basic_string<TCHAR> tstring;

class FakeAPI : public MsiAPI
{
public:
    FakeAPI() : MsiAPI(),
        _lastSession(0),
        _fakeValueSize(0),
        _fakeMsiGetPropertyResult(ERROR_OK),
        _fakeValue(),
        _lastName()
    { }

    virtual ~FakeAPI() { }

    UINT MsiGetProperty(MSIHANDLE session,
        LPCTSTR name, LPTSTR value, DWORD *valueSize)
    {
        _lastSession = session;
        _lastName = name;
        _lastValueSize = valueSize;
        if (!value && valueSize)
        {
            *valueSize = _fakeValueSize;
        }
        else if (value && valueSize)
        {
            ::lstrcpyn(value, _fakeValue.c_str(), *valueSize);
        }
        return _fakeMsiGetPropertyResult;
    }
    void SetFakeMsiGetPropertyResult(UINT value)
    { _fakeMsiGetPropertyResult = value; }
    void SetMsiGetPropertyValueSize(DWORD valueSize)
    { _fakeValueSize = valueSize; }
    void SetMsiGetPropertyValue(tstring const &value)
    { _fakeValue = value; }
    MSIHANDLE MsiGetPropertyLastSession() const
    { return _lastSession; }
    tstring const &MsiGetPropertyLastName() const
    { return _lastName; }

private:
    MSIHANDLE _lastSession;
    UINT _fakeValueSize;
    UINT _fakeMsiGetPropertyResult;
    tstring _fakeValue;
    tstring _lastName;
};

class MyCustomAction
{
public:
    MyCustomAction(MsiAPI &api) : _api(api)
    { }

    UINT Process(MSIHANDLE session)
    {
        TCHAR value[1024];
        DWORD valueSize = sizeof(value)/sizeof(value[0]);
        UINT result = _api.MsiGetProperty(session,
            _T("customProperty"), &value[0], &valueSize);
        return ERROR_OK;
    }

private:
    MsiAPI &_api;
};

// this is the production code called in the real installer
UINT MyCustomActionEntry(MSIHANDLE session)
{
    return MyCustomAction(ProductionApi()).Process(session);
}

// this is how you exercise your CA in a test
void TestMyCustomAction()
{
    FakeApi api;
    MSIHANDLE fakeSession = reinterpret_cast<MSIHANDLE>(0x1);
    UINT result = MyCustomAction(api).Process(fakeSession);
    assert(fakeSession == api.MsiGetPropertyLastSession());
    assert(_T("customProperty") == api.MsiGetPropertyLastName());
}

This is all code I'm typing off the top of my head here, but I've done
things this way and it works.  You will spend some time creating the
mock MSI api one function at a time.  If you already have a class
based wrapper around the MSI API, then you can create an interface for
your wrapper and mock the wrapper instead of the low-level functions.

An alternative to using composition through the _api member in your
CA's implementation is to use a virtual member function that shadows
the global API function.  In your test you derive from your production
class and override the virtual member function to fake out the global
API.  Roughly, it looks like this:

class MyCustomAction
{
public:
	MyCustomAction() { }
	virtual ~MyCustomAction() { }

    UINT Process(MSIHANDLE session)
    {
        TCHAR value[1024];
        DWORD valueSize = sizeof(value)/sizeof(value[0]);
		// note: no use of _api member:
        UINT result = MsiGetProperty(session,
            _T("customProperty"), &value[0], &valueSize);
        return ERROR_OK;
    }

protected:
    virtual UINT MsiGetProperty(MSIHANDLE session,
        LPCTSTR name, LPTSTR value, DWORD *valueSize)
	{ return ::MsiGetProperty(session, name, value, valueSize); }
};

class MyCustomActionTester : public MyCustomAction
{
public:
	MyCustomActionTester(); // initializes all the fake data
	virtual ~MyCustomActionTester() { }

    void SetFakeMsiGetPropertyResult(UINT value)
    { _fakeMsiGetPropertyResult = value; }
    void SetMsiGetPropertyValueSize(DWORD valueSize)
    { _fakeValueSize = valueSize; }
    void SetMsiGetPropertyValue(tstring const &value)
    { _fakeValue = value; }
    MSIHANDLE MsiGetPropertyLastSession() const
    { return _lastSession; }
    tstring const &MsiGetPropertyLastName() const
    { return _lastName; }

protected:
    virtual UINT MsiGetProperty(MSIHANDLE session,
        LPCTSTR name, LPTSTR value, DWORD *valueSize)
	{
        _lastSession = session;
        _lastName = name;
        _lastValueSize = valueSize;
        if (!value && valueSize)
        {
            *valueSize = _fakeValueSize;
        }
        else if (value && valueSize)
        {
            ::lstrcpyn(value, _fakeValue.c_str(), *valueSize);
        }
        return _fakeMsiGetPropertyResult;
    }
};

Then your unit test would instantiate MyCustomActionTester and
configure the fake behavior through MyCustomActionTester's members
instead of through FakeApi's members.

The upshot of all of this is that it provides a level of indirection
between your CA and the MSI API so that you can fake out the API as
needed -- no need to create in-memory databases unless you really need
that.  No need to call the production MSI API *at all*, avoiding the
whole business of "you can't call that because you're not in a
transaction" problem that you get when you try to half-fake out the
MSI API by providing shim databases.

I've done it both ways and the way I'm outlining here, with either the
FakeApi or the xxxTester class, is much easier to control than with
the shim database.
-- 
"The Direct3D Graphics Pipeline" -- DirectX 9 draft available for download
      <http://www.xmission.com/~legalize/book/download/index.html>

        Legalize Adulthood! <http://blogs.xmission.com/legalize/>
date: Tue, 12 Aug 2008 10:34:20 -0700   author:   legalize+ (Richard [Microsoft Windows Installer MVP])

Re: How do I execute deferred CAs programmatically (for unit testing customactions)?   
We scrapped this exact approach because implementing more complex MSI
functions was too much work. We're now using a real manufactured MSI
database.
http://code.dblock.org/ShowPost.aspx?id=7
date: Tue, 19 Aug 2008 16:38:35 -0700 (PDT)   author:   dB.

Re: How do I execute deferred CAs programmatically (for unit testing customactions)?   
[Please do not mail me a copy of your followup]

"dB."  spake the secret code
 thusly:

>We scrapped this exact approach because implementing more complex MSI
>functions was too much work.

...but a deferred CA can only call a handfull of functions, so I don't
see why you have to implement complex MSI functions.  Also, if your mock
MSI functions are becoming complex, then you're just not doing it right.
I've done this plenty of times (mocking out API calls) and its never
been complex.
-- 
"The Direct3D Graphics Pipeline" -- DirectX 9 draft available for download
      <http://www.xmission.com/~legalize/book/download/index.html>

        Legalize Adulthood! <http://blogs.xmission.com/legalize/>
date: Thu, 21 Aug 2008 15:06:20 -0700   author:   legalize+ (Richard [Microsoft Windows Installer MVP])

Google
 
Web ureader.com


    COPYRIGHT 2007, YARDI TECHNOLOGY LIMITED, ALL RIGHT RESERVE  |   contact us