Monday, 27 May 2019

Watching A Folder

Been a while since I published some (hopefully) useful Xojo code, so this time I have quite an interesting one.

There are times when it is necessary to get your application to respond to changes in the file system. This is often used for programmers tools where a directory listing is used rather than storing each of the files in the project file, or if your application needs to process data sent via FTP. There are many other scenarios where this feature could be equally useful, but these are the only two I have had to address my self, so far.

In the .Net world, we have the FileSystemWatcher class that can accomplish this for us quite easily. Unfortunately in Xojo, we do not, so we need to roll our own.

The first thought behind this is to simply create a Timer that periodically checks the folder for changes.

OK. But there are some issues with this:
  1. How does the Timer event know if the folder has been altered?
  2. What happens with a particularly large folder? Just using the Time Action event would hijack the main thread and cause our application to become unresponsive when the checking is being done.
  3. What happens if a subfolder is amended?

There are other potential issues, but these are the main three.

My solution is to create a new class, that I will call FolderWatcher.

Class FolderWatcher

End Class

This class does not need to inherit anything, nor use any interfaces, so simply select the Class option from the Insert menu.

When creating a class that fires events, as this will, I like to set up the events definitions at the start, so right now selective Event Definition option from the Insert Menu and name the definition Action. This doesn’t need any parameters as the FolderItem will be retrievable from the class instance.

Talking of the FolderItem, we ought to set up that property now. From the Insert menu, click Property. Call this property mFolder, set the Type to FolderItem and its scope should be Private.



As I said before, we will need to retrieve the FolderItem during the Action event, so add a Computed Property, called Folder, keep this property Public so we can access it from outside the FolderWatcher instance. In the Get method of the Folder property, simply return the mFolder private property


.
I said before that using a Timer to check the folder would be inefficient and cause our application to lock up. However, we do need a Timer to trigger the file check and to see if the flag (which we will set up in a minute), to indicate the folder has changed, has been set.
So now we create the Timer property of the FolderWatcher. For simplicity sake, let us just call this mTimer and set it’s scope to Private.



We will also need two private flags within the class. So, set up two private Boolean properties (I’m sure you have worked out how to create properties by now) and call them mEnabled and mFolderChanged. Set both of the default values of these properties to False.
We only need to expose the mEnabled property to outside the class, so we do this with a computed Property called Enabled. This is also a boolean property. The Get method of this property simply returns the value of the mEnabled private property. However, the Set method sets the value of mEnabled, but also sets the Enabled property of the mTimer property.

  mEnabled = value
  mTimer.Enabled = value

There are just two more properties to add, so we may as well do that now, before we move on to the methods.

Add a private property called mThread. This is of type Thread and will be used to perform the check of the folder without interrupting the main thread.
Add another private property called mFolderDetails and set this to a String. This will store the structure of the folder for comparison during the execution of mThread.

Where have we got too so far?

By now, you should have a class that looks like this:



If you were to try to use this class right not, it would do nothing. Now we need to add the functionality with methods.

First up, the all important constructor.

Create the constructor like any other method. Select the Method option from the Insert menu.
Name this method Constructor and ensure its scope is set to Public. We also need to pass two parameters to the constructor: A folderItem, we will call Folder and a Boolean we will call AutoStart.
The constructor is where we will set everything up.
Firstly, check that we have actually passed a Folder, and not a file to the constructor. This is simply a case of testing the Directory property of the Folder variable. If this returns False, raise a runtime exception.
Now we have the preliminary check out of the way, we can continue with the set up:
Store the Folder in the classes mFolder property.
Create an instance of the Thread class and configure it. The only configuration we need to do here is set the event handler (which we will create in a moment).
Create an instance of the Timer class, and configure that also. This requires a little more configuration. We set the event handler (again, configured in a moment), the period between each trigger of the Timer Action event, the Mode property of the Time, and whether or not we want the Timer to start immediately.

We should end up with code like this in the constructor:

' This class only watches folders, not individual items, so if the
' item passed is not a folder/directory, raise an exception
If Not Folder.Directory Then Raise New RuntimeException

' Store the folder
mFolder = Folder

' Create a new Thread object and attach the Run handler
mThread = New Thread()
AddHandler mThread.Run, AddressOf ThreadAction

' Create the Timer object and add the Action handler
mTimer = New Timer()
AddHandler mTimer.Action, AddressOf TimerAction

' For now, set this Timer to fire every second
mTimer.Period = 1000
mTimer.Mode = Timer.ModeMultiple

' Enable the Timer if AutoStart os True
mTimer.Enabled = AutoStart

The first thing we need to address (no pun intended) is creating the event handlers for the Thread and the Timer. These are the key methods that allow this class to perform its desired task.

To create the event handler for the Thread, we simply create a new method, as we did for the constructor. This time, we will call the method ThreadAction, we set it to Private and have a single parameter called Sender of type Thread.



This method does not return a value.
The body of this method gets the structure of the folder and compares it to the stored structure. If there is a difference, then the mFolderChanged flag is set to True.

' When the Thread runs, get the string value of the folder structure
Dim NewDetails As String = GetFolderDetails(mFolder)

' If the structure has changed since the last check (or this is the first check)
' update the stored details and set the folder changed flag to true
If NewDetails <> mFolderDetails Then
mFolderDetails = NewDetails
mFolderChanged = True
End If

That is all there is to it. The main work of the thread is actually performed in the method GetFolderDetails, but we will get to that after setting up the event handler for the Timer.

To create the event handler for the Timer, we simply create a method in the class. As we can see in the AddHandler call for mTimer, we need to call this method TimerAction. So, that’s what we will do. Repeat the steps to create the ThreadAction method, but this time, call it TimerAction and have the parameter be of type Timer:



The body of the TimerAction method is just as simple as that of the ThreadAction:
Check the FolderWatcher is enabled, if not, exit the method immediately. If the mFolderChanged flag is set to True, reset it to False and then raise the Action event of our FolderWatcher class. Finally, if mThread is not already running, call the Run method of mThread:

' If the file watcher is not enabled, return from this handler
if Not mEnabled Then Return

' If the folder changed flag is set to true, 
' reset it to false and raise the FolderWatcher.Action event handler
If mFolderChanged Then
mFolderChanged = False
RaiseEvent Action()
End If

' If the thread not is running, call its Run method now
If mThread.State <> Thread.Running Then mThread.Run()

The GetFolderDetails method is the main work horse of the class, this is created as any other method, but this time, we pass a FolderItem as the only parameter and we return a string containing the structure of the folder.
As this is more complex that the other methods, we will go through this in closer detail:

Dim FolderDetails() As String ' An array of the folder contents
Dim Index As Integer = 0 ' The current index of the folder’s children
Dim YieldCount As Integer = 10 ' This is used to reduce the amount of yields

These three variables are initialised at the beginning. The first two should be self explanatory, however, the third will require some explanation.
When we execute a separate thread, we need to periodically yield control back to the main thread to prevent the application locking up. This is particularly important in long running, or potentially long running, threads. From my experience, the standard practice for this is to yield control at the end of each iteration of a loop. However, this can effectively cause the thread to be much slower as the ‘behind the scenes’ code to yield the thread builds up. To prevent this, I have found a yield counter is useful. This decrements down to zero on each iteration, when it hits zero, it yields the execution to the main thread. Depending on the complexity of your thread, you may want the yield counter to start at a higher value (say, 100), or lower, if the loop is particularly complex (possibly 2). 
We can now examine each child of the FolderItem passed to the method. This is done with a simple While loop, existing when the Index is equal to, or greater than the number of children.
 
' Get each child of the folder
While Index < Folder.Count

As this example is using the classic framework, the child items of a FolderItem are number from 1, not 0. So, we increment the Index at the start of the loop.

' Using the Classic Framework, so the FolderItem 
' children are in a 1-based array. Therefore,
' the increment is done at the beginning of the loop
Index = Index + 1
  
Get the name and modification date of the child and create a string based upon this details. In this example, I use the TotalSeconds property of the modification date. Please note, in a real world example, it may also be an idea to get the file size and add that to the string, but we won’t worry for this example.

' Get the name and modification date of the child FolderItem
' (NB: Should also be checking file size)
Dim ChildFolder As FolderItem = Folder.Item(Index)
Dim ChildName As String = ChildFolder.Name + " : " + _
Str(ChildFolder.ModificationDate.TotalSeconds)
  
If this child is a folder, we add a marker to the beginning of the string (in this case , I add “[D] “ and append the contents of that folder. TO get the contents of the folder, simply call this method recursively, passing the child as the FolderItem.

' If the FolderItem is a directory, then Add [D] 
' to the beginning of the name and append the
' structure of that folder by recursively calling this method
If ChildFolder.Directory Then ChildName = “[D] " + ChildName + _
GetFolderDetails(ChildFolder)

Add the final string for the child details to the array of strings that give us our structure .

' Add the details of the FolderItem to the array of details
FolderDetails.Append(ChildName)

Now we do the YieldCounter processing described earlier.

' Decrement the YieldCount
YieldCount = YieldCount - 1

' If the YieldCount is at zero, yield to the next thread 
' to prevent application lock up
If YieldCount = 0 Then
App.YieldToNextThread()
  
' Reset the YieldCount
YieldCount = 10
End If
Wend

Finally, we return the array of strings as a single string.

' Return the details as a single string
Return Join(FolderDetails,"|")


Now, to finish the class, we need to add a Destructor and two convenience methods.

The Destructor is required as shutting down your application with an active Timer or Thread may cause it to crash, and shutdown ungracefully. We want to avoid this for several reasons, including the fact that it doesn’t look very professional if your crashes every time your quit.

A Destructor is created in the same was as a Constructor, however, it does not, and cannot, take any parameters. To create it, just create a method called Destructor with a Public scope:


All this needs to do is call the Stop method of the FolderWatcher:

' Stop the FolderWatcher to prevent errors on application close
Stop()


However, we don’t have a Stop method, se we will create it now.
The Stop method is simply a convenience method that sets the Enabled property to False.
Create this by creating a public method called Stop, that takes no parameters, and give it the following body:

' Stop the FolderWatcher Timer
Enabled = False

To compliment this, we can create a Watch method that does the inverse, setting the Enabled parameter too True.
Create this by creating a public method called Watch, that takes no parameters, and give it the following body:

' Start the File Watcher
Enabled = True

And that is the FolderWatcher class, we should now have a class that looks like this:

Class FolderWatcher

Event Action()

Sub Constructor(Folder As FolderItem, AutoStart As Boolean = False) 
' This class only watches folders, not individual items, so if the
' item passed is not a folder/directory, raise an exception
If Not Folder.Directory Then Raise New RuntimeException
' Store the folder 
mFolder = Folder
' Create a new Thread object and attach the Run handler 
mThread = New Thread()
AddHandler mThread.Run, AddressOf ThreadAction

' Create the Timer object and add the Action handler 
mTimer = New Timer()
AddHandler mTimer.Action, AddressOf TimerAction

' For now, set this Timer to fire every second 
mTimer.Period = 1000
mTimer.Mode = Timer.ModeMultiple
' Enable the Timer if AutoStart is True 
Enabled = AutoStart

End Sub

Sub Destructor()

' Stop the FolderWatcher to prevent errors on application close 
Stop()

End Sub

Private Function GetFolderDetails(Folder As FolderItem) As String

Dim FolderDetails() As String ' An array of the folder contents
Dim Index As Integer = 0 ' The current index of the folderʼs children
Dim YieldCount As Integer = 10 ' This is used to reduce the amount of yields
' Get each child of the folder 
While Index < Folder.Count

' Using the Classic Framework, so the FolderItem
' children are in a 1-based array. Therefore,
' the increment is done at the beginning of the loop 
Index = Index + 1

' Get the name and modification date of the child FolderItem ' (NB: Should also be checking file size)
Dim ChildFolder As FolderItem = Folder.Item(Index)
Dim ChildName As String = ChildFolder.Name + " : " + _
Str(ChildFolder.ModificationDate.TotalSeconds)

' If the FolderItem is a directory, then Add [D]
' to the beginning of the name and append the
' structure of that folder by recursively calling this method
If ChildFolder.Directory Then ChildName = "[D] " + ChildName + _
GetFolderDetails(ChildFolder)

' Add the details of the FolderItem to the array of details 
FolderDetails.Append(ChildName)

' Decrement the YieldCount 
YieldCount = YieldCount - 1

' If the YieldCount is at zero, yield to the next thread 
' to prevent application lock up
If YieldCount = 0 Then
App.YieldToNextThread()

' Reset the YieldCount
YieldCount = 10
End If
Wend

' Return the details as a single string
Return Join(FolderDetails,"|")

End Function

Sub Stop()

' Stop the FolderWatcher Timer 
Enabled = False

End Sub

Private Sub ThreadAction(Sender As Thread)

' When the Thread runs, get the string value of the folder structure 
Dim NewDetails As String = GetFolderDetails(mFolder)

' If the structure has changed since the last check (or this is the first check) 
' update the stored details and set the folder changed flag to true
If NewDetails <> mFolderDetails Then
mFolderDetails = NewDetails
mFolderChanged = True 
End If

End Sub

Private Sub TimerAction(Sender As Timer)

' If the file watcher is not enabled, return from this handler 
If Not mEnabled Then Return
' If the folder changed flag is set to true, reset it to false and raise the FolderWatcher.Action event handler
If mFolderChanged Then 
mFolderChanged = False 
RaiseEvent Action()
End If

' If the thread not is running, call its Run method now
If mThread.State <> Thread.Running Then mThread.Run()

End Sub

Sub Watch()

' Start the File Watcher 
Enabled = True

End Sub

Enabled As Boolean
Get
return mEnabled
End Get
Set
mEnabled = value
mTimer.Enabled = value
if mThread.State = Thread.Running Then mThread.Kill()
End Set 
End Property

Folder As FolderItem
Get
Return mFolder
End Get 
End Property

mEnabled As Boolean = False 
mFolder As FolderItem 

mFolderChanged As Boolean = False 

mFolderDetails As String

mThread As Thread

mTimer As Timer 

End Class


So, after all that, how do we use the FolderWatcher class?

Add a FolderWatcher property to your application
Create an instance with the folder you wish to watch
Add an event handler
Respond to the event handler

Or in a little more detail, If we are adding the FolderWatcher to our main window to watch the Desktop folder:
Create a new property of type FolderWatcher
In the Open event of your Window, create an instance of FolderWatcher, passing the folder you wish to watch
Add an event handler to the Action event of the FolderWatcher
Call the Watch method of the FolderWatcher

mFolderWatcher = New FolderWatcher(SpecialFolder.Desktop)
AddHandler mFolderWatcher.Action, AddressOf FolderWatcherAction
mFolderWatcher.Watch()

I have provided a download to a small demonstration application that watches your desktop. Run it and then alter your files on the desktop and it should respond.



Tuesday, 27 March 2018

Prevent CSS and Javascript caching

Quite often, during the early days of a web site's/application's life, a developer can make minor corrections to JavaScript and CSS, whether to correct an issue, or in response to customer feedback.
Unfortunately, nearly all browsers cache these files to facilitate faster loading times during a revisit to a site. Whilst this is desirable for normal day to day use, it can be a developer's nightmare as they have to let the users, if known, know to refresh their cache, or if unknown, find another way to communicate this message.
However, there is a way around this.

The answer is known as using a phantom query string. Like requests for HTML pages, it is possible to append a query string to the references of the scripts and stylesheets. If these are unique on each call, then the file is reloaded from the internet, rather than the cache.
To accomplish this, it is as easy as adding a date and time stamp to the query part of the reference:
e.g.

    <script src="Scripts/script.js?636577391632578321"></script>
    <link href="Styles/style.css?636577391632578321" rel="stylesheet" />

If this date and time stamp is refreshed on each page load, then we can ensure that the file is also refreshed.
To accomplish this in PHP, we can use code like this:

    <link href="Styles/style.css?<?php echo time(); ?>" rel="stylesheet" />

Whereas in ASP/MVC, this will accomplish a similar task:

    <link href="Styles/style.css?@DateTime.Now.Ticks" rel="stylesheet" />

Use the same technique for JavaScript files.

Sunday, 18 March 2018

VS Code save as Admin

For about 2 years now, VS Code has been my go to text editor. It has replaced BBEdit for standard text and NetBeans for web development. I even prefer it to MySQL Workbench when working with databases.
Part of the appeal of VS Code is its extensibility and the growing number of plugins that allow me to do almost anything.
However, I have had to keep BBEdit around for one particular scenario: saving system configuration files, or any file that requires elevated privileges. On Windows, this is not such an issue, as I normally run VS Code as Administrator, but on a Mac it has caused problems.
I was perusing the forums to see if there was a plugin to allow this feature, but no luck. Each of the posts and issues I read that were related to this, seemed to say that it wasn't going to happen for some time. So, BBEdit has to remain.

For clarity, the only reason I decided to go away from BBEdit was the lack of debugging features.

Out of curiosity, and to prove to myself, that this is still an issue, I opened VS Code this morning and attempted to edit my Hosts file, and then resave it... but this happened:


Did I miss this in the change log?
How long has this been here?

I clicked the 'Retry as Admin...' button, and low and behold, I was asked for my password and the file was saved.
Well done Microsoft (and community), you have now managed to convert me over to VS Code completely.


Saturday, 17 March 2018

Regular Expressions in Swift 4

I have recently been looking at converting some of my Xojo code to Swift, more of an exercise to see how easy it is, rather than anything productive.
However, I found that using regular expressions in Swift was not as straight forward as in some languages, like Xojo, Javascript and C#.

The following example will show what you need to do to find a pattern in a string and print each match to the console:

if let regPattern = try? NSRegularExpression(pattern: pattern, options: []) {
    let matches = regPattern.matches(in: text, options: [],
                                     range: NSRange(location:0, length: text.count))
    for match in matches {
        for n in 0..<match.numberOfRanges {
            let range = match.range(at: n)
            let start = self.index(self.startIndex,
                                   offsetBy: range.lowerBound)
            let end = self.index(self.startIndex,
                                 offsetBy: range.upperBound)
            let substring = String(self[start..<end])
            print(substring)
        }
    }

}

As you can see, this is a lot to do for a simple regex search.

To simplify this, I have created some extension methods for the String class, that allow you to accomplish this with far less effort. These methods overload each other, to allow reducing complexity, whilst still allowing the full range of options, should it be required:

extension String {

    func regex(_ pattern:String, options: NSRegularExpression.Options,
               matchingOptions: NSRegularExpression.MatchingOptions,
               range: NSRange) -> [[String]] {
        var result = [[String]]()
        if let regPattern = try? NSRegularExpression(pattern: pattern,
                                                     options: options) {
            let matches = regPattern.matches(in: self, options:
                matchingOptions, range: range)
            for match in matches {
                var matchArray = [String]()
                for n in 0..<match.numberOfRanges {
                    let range = match.range(at: n)
                    let start = self.index(self.startIndex,
                                           offsetBy: range.lowerBound)
                    let end = self.index(self.startIndex,
                                          offsetBy: range.upperBound)
                    let substring = String(self[start..<end])
                    matchArray.append(substring)
                }
                result.append(matchArray)
            }
        }
        return result
    }

    func regex(_ pattern:String, options: NSRegularExpression.Options,
               matchingOptions: NSRegularExpression.MatchingOptions)
        -> [[String]] {
        return self.regex(pattern, options: options,
                          matchingOptions: matchingOptions,
                          range:NSRange(location:0, length:self.count))
    }

    func regex(_ pattern:String, options: NSRegularExpression.Options)
        -> [[String]] {
        return self.regex(pattern, options: options, matchingOptions: [])
    }

    func regex(_ pattern:String) -> [[String]] {
        return self.regex(pattern, options: [])
    }

}

These methods return an array of match arrays. In each of the match arrays, the first element is the entire match, whilst each of the subsequent elements match the capture blocks from the regular expression.
Working from the top to bottom, the methods reduce the complexity exposed to the user, allowing them to only use the options required.
If we start with the first method, you can see that it takes the full range of options for a regular expression search. This can be called by:

let matches = text.regex(pattern, options: [],
                         matchingOptions: [],
                         range: NSRange(location:0, length:text.count))

The next method assumes the range is the full size of the string:

let matches = text.regex(pattern, options: [],
                         matchingOptions: [])

The next method assumes that the matching options are the default:

let matches = text.regex(pattern, options: [])

The final method assumes the regular expressions requires the default options:

let matches = text.regex(pattern)

As you can see, we can reduce the code down to a very manageable call.

To see the results of the match, we can simply iterate over the returned array:

for match in matches {
    for item in match {
        print(item)
    }
}

Now we no longer need to mess around with the ranges and manually extract the strings from the text.

Wednesday, 12 July 2017

To var or not to var

Bit of a change for this post.

When C# 3.0 was released by Microsoft, the var keyword was introduced.
This new keyword allowed the implicit typing of variables, according to their context in the source code. Unfortunately, this new feature has caused some confusion and some problems with readability of code. Therefore, this brief document aims to outline what var is, how it works and when (and when not) to use it.

What is var?

var allows us, as programmers, to reduce the amount of typing when declaring variables. For value types, this doesn’t really make much difference, but for complex reference types, this can reduce the amount of typing a great deal:

e.g.
dictionary<string,dictionary<string,List<int>>> complexItem = new dictionary<string,dictionary<string,List<int>>>();
can be replaced with:
var complexItem = new dictionary<string,dictionary<string,List<int>>>();

Not only does this reduce typing, and slightly enhance readability, it also reduces the possibility of errors in the declaration as we are only having to type out the structure of the type once.
var does this inference at compile time, not runtime. This means that when the compiler finds the above line, it translates it to:

dictionary<string,dictionary<string,List<int>>> complexItem = new dictionary<string,dictionary<string,List<int>>>();

This means that the variable created is not the same as a dynamic variable, and it can only be used in this way if the type is obvious (to the compiler) at compile time.

When not to use var?

var cannot be used in a declaration of a variable if the variable is not initialised:
var unintialisedVariable; // Compile time error
var should also not be used if the type is not obvious when reading the source code:
var newVariable = functionReturningAValue(); // What does this function return?
In this situation, the compiler will be able to determine the type, but it is not immediately obvious to anyone reading the source code. Therefore, the reader would have to find the function declaration to determine the type of the variable.



Saturday, 12 March 2016

Komodo IDE and PHPUnit

For about 2 years now, I have been using Komodo IDE from ActiveState for web development, and on the whole I have been very satisfied with it. There are a couple of issues that have arisen, but normally there is some way to get around them with a minor change of workflow.
However, one issue has consistently bothered me, so I thought it was time I investigated a way to fix the issue: PHPUnit testing.
Out of the box, Komodo will not do unit testing for PHP, so the first thing I had to do was download PHPUnit. Do do this, I needed to install wget using home-brew (for more information about this, go to http://brew.sh).
So, once wget is installed, I followed the instructions on the PHPUnit website to install PHPUnit globally so that it would be accessible no matter where it was called from:

wget https://phar.phpunit.de/phpunit.phar
chmod +x phpunit.phar
sudo mv phpunit.phar /usr/local/bin/phpunit

Thinking that would be it, I created a small test case:

<?php 
class TestClass extends PHPUnit_Framework_TestCase { 
public function testMethod() {  
      $this->assertTrue(TRUE);  
} 
} 
?>

With a simple test plan:



Executing the test, however, resulted in no output. Obviously something was wrong.
After trawling through the Komodo forums and StackOverflow, and little bit of Googling... I came across an unofficial fix for this on GitHub: Komodo PHPUnit Harness
I had found other solutions, but this one seemed the simplest, and I like simple... less to go wrong.

After download the source from the github, and quickly reading the documentation, I realised that I needed to move the PHPUnit executable to part of the Komodo application package. Simply done with this:

sudo mv /usr/local/bin/phpunit '/Applications/Komodo IDE 9.app/Contents/Resources/python/komodo/harnesses/php/Base/PHPUnit.phar'

Note: if you are just doing this without making the mistake I originally made, you don't need to move PHPUnit to /usr/local/bin, just copy it directly to '/Applications/Komodo IDE 9.app/Contents/Resources/python/komodo/harnesses/php/Base'

Then I copied the new versions of HarnessSelector.php and Printer.php into the same folder, over-writing the existing ones.

Again I tried the test plan in Komodo:


Yay! Success!

Now only two things remain:

  1. An easy way to create unit test modules
  2. Code completion for the unit test classes

The first part is easy. I created an empty test case class, basically the same as the class I created before. This I then saved in the templates folder: ~/Library/Application Support/KomodoIDE/9.3/templates/My Templates.
Now I can create a new test case file from the File Menu in Komodo, by selecting New->File From Template...

The code completion is a little trickier, but was still quite simple. Komodo has the ability to read in files from a folder to aid code completion. The setting for this can be found in the Preferences window of Komodo:


This setting allows us to point to the PHPUnit source, however, I didn't have that yet... so back to Github to download the source: PHPUnit

Once I had that, and unzipped it, I wanted to keep everything neat and self contained, so I copied the relevant folder from the source to the test harness within the Komodo package. I only really needed the Framework folder in the source, so that's all I copied to /Applications/Komodo IDE 9.app/Contents/Resources/python/komodo/harnesses/php.

Unfortunately, the preferences window wouldn't allow me to select a folder within a bundle, so I ended up editing the prefs.xml stored in ~/Library/Application Support/KomodoIDE/9.3 manually. I found the String node of the XML with the id attribute of phpExtraPaths, and changed the  value of that node to: /Applications/Komodo IDE 9.app/Contents/Resources/python/komodo/harnesses/php/Framework

I actually did this last step twice because I had Komodo open the first time and it restored the original setting when I closed it. So, if you are doing this, make sure Komodo is not open when you edit the prefs.xml.

As you can see in the following shot, I now have code completion for PHPUnit.


The downside of all this is that I will need to redo these steps whenever Komodo IDE is updated, but it does now allow me to enjoy the benefits of unit testing in my PHP projects from within the Komodo IDE.