2020-03-09

Unit test coverage in Visual Studio for Mac

I've recently started working on a new project using Visual Studio for Mac to write apps for iOS and Android in C#. I'll try to write about this new project in the future, but for now I wanted to talk about measuring and viewing code coverage for my unit tests.

Although Visual Studio for Windows does have code coverage measurements and reporting built-in, VS for Mac does not. It does support running unit tests, via NUnit or xUnit, but only shows information about tests passing or failing.

I did a bit of searching about found the VSMac-CodeCoverage extension for VS, but when I tried it install it failed. It looks like there has been a new release which may solve that, but I haven't had a chance to try yet. [Update: that extension has been fixed and does work now.]

Because the above extension didn't work for me when I tried, I can up with a different usable solution. It isn't perfect, but it does allow me to run my tests, generate a code coverage report, and view it as HTML with a single menu option. Here's what I did:

  1. In your unit tests project add the following two nuget packages:
    1. coverlet
    2. ReportGenerator
  2. Verify that you can run tests from the command line by running this in the folder of your unit tests project:
    dotnet test --collect:"XPlat Code Coverage"
  3. Verify that you can generate reports from the command line:
    ~/.dotnet/tools/reportgenerator -reports:TestResults/0cf0a5da-b2a9-46b1-9fe8-78ae83743fd7/coverage.cobertura.xml -targetdir:CoverageReport
    The long ID will be different for each test run.
    Then view the generated report:
    open CodeCoverage/index.htm
  4. If the above worked, then you just need to script to semi-automate this. Here's my script:
    #!/bin/zsh
    dotnet test --collect:"XPlat Code Coverage"
    ~/.dotnet/tools/reportgenerator -reports:TestResults/`ls -1t TestResults | head -1`/coverage.cobertura.xml -targetdir:./CodeCoverage
    open CodeCoverage/index.htm
  5. Finally, add a custom tool to the Tools menu: Tools → Edit Custom Tools...
You can then run the tests to generate coverage data, generate the coverage report, and open it in your browser just by running this new command from the Tools menu. Note that you must run the command when one of your unit test files is opened because it uses the ${ProjectDir} as the working directory and the unit test runner must run in a unit test project.

2015-03-01

Facebook Integration


I've spent the past few weeks adding Facebook integration to Zoing. This took much longer than I expected, but it is working now and I only need to finish testing and release. I didn't carefully document why this took longer than expected, but I still want to write a little about my experience in case it might help others.

You can see what this looks like (for iOS) in the picture to the right. Below that is how it ends up in your Facebook timeline.

First, as I've written before, I'm using Xamarin to develop on both iOS and Android in C#. This allows me to work in a language I'm proficient in, and reuse a lot of the code. This works really well for areas of the code that are platform agnostic, such as the core logic for my game. It also works quite well for areas that have a good common library, such as using OpenTK, which is a C# wrapper for OpenGL. But, sharing code becomes less useful when a library is not consistent across platforms, which is the case for Facebook's SDK for iOS and Android.

I can't think of any good reason that Facebook's SDK is so different for these two platforms. Sure there are differences in the way that most Apple libraries work and Android libraries work. Some of that is due to best practices for Objective C versus those for Java. But, I don't see the differences in Facebook's library is being beneficial in any way. It means that anyone who wants to integrate Facebook into their apps essentially needs to learn how to use the library twice.

Making this even worse is that the documentation from Facebook is not very good. It is quite extensive, but just not good. There are very few examples, and they are hard to find. The one I found that ended up helping me the most was the sample game they have, FriendSmash. They have a complete sample project for both iOS and Android.

I should mention that this problem is made even more complex for me because I'm also using Xamarin's Facebook SDK wrapper, which is available in separate versions for iOS and Android. From my perspective Xamarin would ideally provide a single library for Facebook integration that has a consistent API, and that there are platform specific layers to have that library communicate with Facebook's iOS and Android libraries.

One Library to Rule Them All

To make my life easier, this is exactly what I set out to create. I'm currently only using a small subset of all Facebook functionality. Basically I only need to: login and logout, retrieve the user's profile image, and post a custom story. There were two key reasons I wanted this single cross-platform library. One was because I also plan to integrate other social networks, at least Twitter and maybe others. Ideally I can interface with these via a consistent API. The second was because most of my code is platform agnostic and can't call any platform specific functions. Creating this library allows me to perform these Facebook actions from within my code.

To support this I created the following abstract class:
using System;

namespace Qythyx.Infrastructure.SocialNetwork
{
    /// <summary>Provides functionality related to a social network platform.</summary>
    public abstract class SocialNetworkConnector
    {
        private bool _isLoggedIn;

        /// <summary>Initializes a new instance of <see cref="SocialNetworkConnector"/>.</summary>
        protected SocialNetworkConnector()
        {
            Login(false, _ => {});
        }

        /// <summary>Gets a value indicating whether this <see cref="SocialNetworkConnector"/> is displaying dialog.</summary>
        /// <value><c>true</c> if displaying a dialog; otherwise, <c>false</c>.</value>
        public bool DisplayingDialog { get; private set; }

        /// <summary>Logs in to the social network and then calls the specified callback.</summary>
        /// <param name="showUI">Determines if the login UI should be shown or not.</param>
        /// <param name="callback">An action that is called after the login completes
        /// with either <c>true</c> if the login was successful; otherwise <c>false</c>.</param>
        public void Login(bool showUI, Action<bool> callback)
        {
            DisplayingDialog = showUI ? true : false;
            PerformLogin(
                showUI,
                success =>
                {
                    if(!success)
                    {
                        Logout();
                    }
                    IsLoggedIn = success;
                    callback(success);
                    DisplayingDialog = false;
                }
            );
        }

        /// <summary>Logs in to the social network and then calls the specified callback.</summary>
        /// <param name="showUI">Determines if the login UI should be shown or not.</param>
        /// <param name="callback">An action that is called after the login completes
        /// with either <c>true</c> if the login was successful; otherwise <c>false</c>.</param>
        protected abstract void PerformLogin(bool showUI, Action<bool> callback);

        /// <summary>Logs out of the social network.</summary>
        public void Logout()
        {
            PerformLogout();
            IsLoggedIn = false;
        }

        /// <summary>Logs out of the social network.</summary>
        protected abstract void PerformLogout();

        /// <summary>Gets or sets a value indicating whether the user is logged in to the social network.</summary>
        /// <value><c>true</c> if logged in; otherwise, <c>false</c>.</value>
        public bool IsLoggedIn
        { 
            get
            {
                return _isLoggedIn;
            }
            set
            {
                _isLoggedIn = value;
                if(value)
                {
                    GetUserImage(
                        (success, userImage) =>
                        {
                            UserImage = userImage;
                            if(LoggedInStateChangeEventHandler != null)
                            {
                                LoggedInStateChangeEventHandler(true, UserImage);
                            }
                        }
                    );
                }
                else
                {
                    if(LoggedInStateChangeEventHandler != null)
                    {
                        LoggedInStateChangeEventHandler(false, null);
                    }
                }
            }
        }

        /// <summary>The user's image.</summary>
        public ShareDetails UserImage { get; private set; }

        /// <summary>Occurs when logged in state changes.
        /// The <c>bool</c> determines if the user is logged in.
        /// In general if the user is logged in then the <see cref="ShareDetails"/> should hold the user's image,
        /// but if there was a problem retrieving it then this will be <c>null</c>.
        /// </summary>
        public event Action<bool, ShareDetails> LoggedInStateChangeEventHandler;

        /// <summary>Attempts to login if necessary and, if logged in, performs the given action.</summary>
        /// <param name="action">The action to perform if login is successful.</param>
        public void ConfirmLoginAndDoAction(Action action)
        {
            if(!IsLoggedIn)
            {
                Login(true, success => { if(success) action(); });
            }
            else
            {
                action();
            }
        }

        /// <summary>Retrieves the user's profile image from the social network and, once it is retrieved, passes it to the callback.
        /// The callback is passed a boolean denoting if retrieval was successful and the image information. The image format is RGBA.
        /// If retrieval fails the image data will be <c>null</c>.</summary>
        /// <param name="callback">A callback that is called after retrieval completes.</param>
        protected abstract void GetUserImage(Action<bool, ShareDetails> callback);

        /// <summary>Shares the details of completing a level to the social network.</summary>
        /// <param name="details">The <see cref="ShareDetails"/> describing the accomplishment.</param>
        public abstract void ShareLevelCompletion(ShareDetails details);
    }
}
This defines all of the functionality I need for Facebook. Unfortunately, to implement this for iOS and Android takes quite a bit of code, and as I said above, it's quite different for each platform. Also, unfortunately, to hook in the functionality for each platform requires bits and pieces of code or configuration in multiple files, which is kind of a pain. I'll try to explain what I did to get this working on both platforms.

iOS

AppDelegate.cs

You need to initialize the Facebook library during you app startup and the recommended place to do that is in the AppDelegate.FinishedLaunching method by adding the following. You'll also need to add using MonoTouch.FacebookConnect; to this file.
FBSettings.DefaultAppID = "your Facebook app id";
FBSettings.DefaultDisplayName = "your Facebook app display name";
So, where does that app id come from? Well, that's another important piece of setting up Facebook integration. This is explained in Facebook's "getting started" documentation for iOS and Android, although reading through the instructions left me with a lot of questions that took a while to finally resolve.

Info.plist

Next you need to add your app ID to your Info.plist file. This is also explained in Facebook's documentation, although it also could be clearer.

FacebookConnector.cs

After this I needed to implement the iOS version of my SocialNetworkConnector:
using CoreGraphics;
using Foundation;
using MonoTouch.FacebookConnect;
using System;
using System.Collections.Generic;
using System.Globalization;
using UIKit;
using Qythyx.Infrastructure.Extensions;
using Qythyx.Infrastructure.SocialNetwork;

namespace Qythyx.ZoingiOS
{
    /// <summary>Provides functionality related to a Facebook.</summary>
    public sealed class FacebookConnector : SocialNetworkConnector
    {
        private static readonly string[] _ExtendedPermissions = new [] { "user_about_me", "publish_actions" };

        /// <summary>Initializes a new instance of <see cref="FacebookConnector"/>.</summary>
        public FacebookConnector()
            : base()
        {
        }

        /// <summary>Logs in to the social network and then calls the specified callback.</summary>
        /// <param name="showUI">Determines if the login UI should be shown or not.</param>
        /// <param name="callback">An action that is called after the login completes
        /// with either <c>true</c> if the login was successful; otherwise <c>false</c>.</param>
        protected override void PerformLogin(bool showUI, Action<bool> callback)
        {
            FBSession.OpenActiveSession(
                _ExtendedPermissions,
                showUI,
                new SessionStateHandler(callback).FBSessionStateHandler
            );
        }

        /// <summary>Logs out of the social network.</summary>
        protected override void PerformLogout()
        {
            FBSession.ActiveSession.CloseAndClearTokenInformation();
        }

        /// <summary>Retrieves the user's profile image from the social network and, once it is retrieved, passes it to the callback.
        /// The callback is passed a boolean denoting if retrieval was successful and the image information. The image format is RGBA.
        /// If retrieval fails the image data will be <c>null</c>.</summary>
        /// <param name="callback">A callback that is called after retrieval completes.</param>
        protected override void GetUserImage(Action<bool, ShareDetails> callback)
        {
            FBRequest.ForMe.Start(
                new RequestHandler(
                    (success, result) =>
                    {
                        ShareDetails details = null;
                        if(success)
                        {
                            FBGraphObject userInfo = (FBGraphObject)result;
                            string imageUrl = String.Format(
                                CultureInfo.InvariantCulture,
                                "https://graph.facebook.com/{0}/picture?width={1}&height={1}", 
                                userInfo["id"].ToString(),
                                Zoing.Constants.SocialUserImageSize
                            );
                            details = GetFacebookUserImage(imageUrl);
                        }
                        callback(success, details);
                    }
                ).FBRequestHandler
            );
        }

        private static ShareDetails GetFacebookUserImage(string imageUrl)
        {
            try
            {
                NSData data = NSData.FromUrl(new NSUrl(imageUrl));

                using(CGImage image = UIImage.LoadFromData(data).CGImage)
                {
                    int scaledWidth = ((Int32)image.Width).NearestSmallerPowerOf2();
                    int scaledHeight = ((Int32)image.Height).NearestSmallerPowerOf2();

                    byte[] pixels = new byte[scaledWidth * scaledHeight * 4];
                    using(CGBitmapContext context = new CGBitmapContext(pixels, scaledWidth, scaledHeight, 8, 4 * scaledWidth, CGColorSpace.CreateDeviceRGB(), CGBitmapFlags.NoneSkipLast))
                    {
                        context.DrawImage(new CGRect(0, 0, scaledWidth, scaledHeight), image);
                        return new ShareDetails(String.Empty, pixels, scaledWidth, scaledHeight);
                    }
                }
            }
            catch(Exception)
            {
                return null;
            }
        }


        private static void StageImage(UIImage image, Action<bool, string> completionAction)
        {
            // Using FBRequestConnection.UploadStagingResource fails with a cast exception, but the following works.
            FBRequest request = FBRequest.ForUploadStagingResource(image);
            request.Start(
                new RequestHandler(
                    (success, result) => completionAction(success, success ? ((FBGraphObject)result)["uri"].ToString() : null)
                ).FBRequestHandler
            );
        }

        /// <summary>Shares the details of completing a level to the social network.</summary>
        /// <param name="details">The <see cref="ShareDetails"/> describing the accomplishment.</param>
        public override void ShareLevelCompletion(ShareDetails details)
        {
            ObjCRuntime.Class.ThrowOnInitFailure = false;
            UIImage image = CreateUIImage(details);
            StageImage(
                image,
                (success, url) =>
                {
                    if(success)
                    {
                        ShareLevelCompletion(details.Message, image, url);
                    }
                }
            );
        }

        private static UIImage CreateUIImage(ShareDetails details)
        {
            const int facebookSize = Qythyx.Zoing.Constants.FacebookImageSize;
            CGBitmapContext context = new CGBitmapContext(null, facebookSize, facebookSize, 8, 0, CGColorSpace.CreateDeviceRGB(), CGBitmapFlags.NoneSkipLast);

            using(
                CGImage image = new CGImage(
                    details.Width,
                    details.Height,
                    8,
                    32,
                    details.Width * 4,
                    CGColorSpace.CreateDeviceRGB(),
                    CGBitmapFlags.None,
                    new CGDataProvider(details.Image, 0, details.Image.Length),
                    null,
                    false,
                    CGColorRenderingIntent.Default
                )
            )
            {
                double scale = Math.Max((double)details.Width / facebookSize, (double)details.Height / facebookSize);
                int scaledWidth = (int)(details.Width / scale);
                int scaledHeight = (int)(details.Height / scale);
                int left = (facebookSize - scaledWidth) / 2;
                int top = (facebookSize - scaledHeight) / 2;
                context.InterpolationQuality = CGInterpolationQuality.High;
                context.DrawImage(new CGRect(left, top, scaledWidth, scaledHeight), image);
            }

            return new UIImage(context.ToImage());
        }
        
        private static void ShareLevelCompletion(string title, UIImage image, string imageUrl)
        {
            // From https://developers.facebook.com/docs/applinks/hosting-api
            const string url = "https://fb.me/_____your app's URL, see above link____";

            var fbOpenGraphObject = FBGraphObject.OpenGraphObjectForPost(
                "zoing-game:game_level",
                title,
                new NSString(imageUrl),
                new NSString(url),
                Qythyx.Zoing.Constants.ZoingDescription
            );
            
            FBGraphObject graphObject = (FBGraphObject)ObjCRuntime.Runtime.GetNSObject(fbOpenGraphObject.Handle);

            var graphAction = FBGraphObject.OpenGraphAction;
            graphAction.SetObject(new NSString("game_level"), "preview property");
            graphAction.SetObject(graphObject, "game_level");
            graphAction.SetObject(new NSString("true"), "fb:explicitly_shared");
            graphAction.SetObject(image, "image");

            FBOpenGraphActionParams fbParams = new FBOpenGraphActionParams(graphAction, "me/zoing-game:complete", "game_level");

            if(FBDialogs.CanPresentShareDialog(fbParams))
            {
                FBDialogs.PresentShareDialog(
                    graphAction, 
                    "zoing-game:complete",
                    "game_level",
                    (call, results, error) =>
                    {
                        // do nothing, either the share was successful or not, nothing to do about it
                    }
                );
            }
            else
            {
                Dictionary<string, object> webParameters = new Dictionary<string, object>() {
                    { "name", title },
                    { "caption", "Zoing" },
                    { "description", Qythyx.Zoing.Constants.ZoingDescription },
                    { "link", url },
                    { "picture", "https://sites.google.com/a/qythyx.com/www/Zoing%20Icon%20180x180.png" },
                };

                var s = GetNSDictionary(webParameters)["picture"];

                // FBWebDialogs.PresentDialogModally failed with an exception, so using this instead.
                FBWebDialogs.PresentFeedDialogModally(
                    FBSession.ActiveSession,
                    GetNSDictionary(webParameters),
                    (result, resultsUrl, error) => { /* do nothing */ }
                );
            }
        }

        private static NSDictionary GetNSDictionary(Dictionary<string,object> dictionary)
        {
            NSMutableDictionary nsd = new NSMutableDictionary();
            foreach(var entry in dictionary)
            {
                nsd.Add(new NSString(entry.Key), NSObject.FromObject(entry.Value));
            }
            return nsd;
        }

        private class RequestHandler
        {
            private readonly Action<bool, NSObject> _completionHandler;

            public RequestHandler(Action<bool, NSObject> completionHandler)
            {
                _completionHandler = completionHandler;
            }

            public void FBRequestHandler(FBRequestConnection connection, NSObject result, NSError error)
            {
#if DEBUG
                if(error != null)
                {
                    Console.WriteLine("Facebook Request Error: " + error.ToString());
                    if(connection.UrlResponse != null)
                    {
                        Console.WriteLine("Facebook URLResponse: " + connection.UrlResponse.ToString());
                    }
                }
#endif
                _completionHandler(error == null, result);
            }
        }

        private sealed class SessionStateHandler
        {
            private readonly Action<bool> _action;

            public SessionStateHandler(Action<bool> action)
            {
                _action = action;
            }

            public void FBSessionStateHandler(FBSession session, FBSessionState state, NSError error)
            {
#if DEBUG
                if(error != null)
                {
                    Console.WriteLine("Facebook Request Error: " + error.ToString());
                }
#endif
                _action(SessionStateIsLoggedIn(state));
            }
        }

        private static bool SessionStateIsLoggedIn(FBSessionState state) 
        {
            return state != FBSessionState.ClosedLoginFailed && state != FBSessionState.Closed && state != FBSessionState.CreatedOpening;
        }
    }
}

Hooking It All Together

Obviously there's a bit more to it than the above. For example, attaching something to the base class's Info.plistLoggedInStateChangeEventHandler to actually use the user's image that is returned. All of that stuff will be different and specific to each app and I can't document exactly how everyone should use this.

Anyway, I hope this helps someone else. I'll try to document the Android version soon. It's a bit more complex, but does implement my consistent abstract class, and after hooking everything up correctly, works pretty well.

2015-02-01

Building for ARM64

Now that I finally released the Android version of Zoing and Zoing Zero to the Google Play Store, I now can finally start working on some new features. But, before doing that I need to provide 64 bit support, as described by Apple. Fortunately, Xamarin makes this pretty easy. I recently updated my game to the Unified API, which includes support for ARM 64, and was entirely painless.

Aside from the requirement to do this, I also wondered how much of a performance improvement it might give me. I benchmarked Zoing built for ARM7, ARM7s, and ARM64. All of these runs were done on my iPhone 6 Plus.

Clearly the build that includes ARM64 does provide a huge performance improvement (average FPS of 622 compared to a best of 482 for previous builds). I find it interesting that the ARMv7s does not provide any benefit of ARMv7, but again this was testing on a iPhone 6 Plus, which contains the ARM64. Perhaps if I tested on an iPhone 5S I would have seen something different.

I should also explain these FPS numbers. iOS actually limits OpenGL animations to a maximum of 60 FPS. For the purposes of this benchmark I run my game's logic loop as fast as I can, but show the screen only once every 1,000 logic-loop cycles. This allows me to benchmark my code's efficiency as well as these CPU changes.

I will try to blog a little more often going forward, but I've said that before too. ;-)

Finally, Android Is Published!

Sorry for the long hiatus in writing, all I can say is that I've been busy. The good news is that I have nice results from that busyness. I've finally released the Android version of Zoing. There are two options: Zoing for $0.99 or Zoing Zero for free with ads.

Now that I've finished the Android version I hope to be able to iterate on adding to features to both iOS and Android pretty quickly. Keep watching this blog or the new Zoing page on Facebook for news. I'm also tweeting under ZoingGame, so feel free to follow me there as well.

2014-06-01

Back to Optimizing

It's been a while since I lost wrote, but I have a good excuse, I've been focusing on creating the Android version of my game. The good news is that everything is working now. It's running on both Android 2.3+ and a slightly improved version for Android 4.4+ (it shows in full-screen). What I'm working on now is optimizing it to get the performance I want. I've written articles about optimization in the past, so I'll keep this short and to the point of what I've done so far.

Measure, Measure, Measure

I've written about this in the past, but I've found that when you're trying to optimize something it is extremely important to measure often and enough. Also, optimizing for a focused measurement doesn't always result in improvements in the real environment.

To try to do a better job at measuring I've been using histograms of the speed of what I'm measuring to get a better picture than just an average speed. I'm also using the profiler functionality in Visual Studio, but I can only use that for the PC version of my game, not directly against the Android binary. Still this led to useful insight.

Two Optimizations

When I profiled my code I found two areas I thought I could improve relatively easily. The first was an unnecessary calculation when I manipulate (translate or rotate) quads. I was recalculating the center of the quads even though for these operations that wasn't necessary. This was pretty straight-forward to improve and resulted in a significant speedup. You can see this in the picture to the right: PC-L-SS is the original version, and PC-L-SS-Opt Mutable is this first optimization.

After the above I ran the profiler again and determined that a significant amount of time was being spent on calculating sine and cosine operations. I did some Google searching and found an interesting discussion that describes a faster way to calculate sine (the source of this information contains more details). So, I implemented this alternative sine calculation and measured again. This resulted in the PC-L-SS-FastSin numbers in the histogram.

The average FPS numbers for the above are: original = 1,114, first optimization = 1,380, and second optimization = 1,411. Note that this is just running my game logic code, but not actually rendering via OpenGL. I did this in order to focus as much as possible on the area I was optimizing.

But What About Android?

So, the above is interesting and all, but I'm trying to optimize Android, not my PC version. So, since I can't use the profiler, I can just run the same measurements. This resulted in the histogram to the right.

Clearly from this both the first optimization (Opt Mutable) and the second additional optimization (Opt Sine) are significantly faster than the original. To summarize this even more, the average FPS for each are: original = 37.29, first optimization = 42.95, second optimization = 43.45. In this case the numbers are both the game logic and the actual rendering. Because I'm trying to evaluate the true game performance, in my attempt to get to 60 FPS, I thought this was more useful.

Still More Work to Do

While I'm pretty happy with the results of this initial optimization, it's still not performing as well as I want. The Android measurements above were done on a Nexus 4, and I'd really like it to run at 60 FPS with some headroom for slower devices. My profiler investigations have given me some ideas for more optimizations, but I expect them to be more difficult than the above.

2014-04-16

And...my first AppStore update

I pushed a minor update to Zoing to the AppStore on Saturday and it just went live. It's not particularly interesting in terms of changes to the game, but it's exciting for me given this is my first one ever.

The actual update just rearranges the icons on the start screen to try to make it easier to use. Now I'm working on Android support, so I don't expect any new iOS updates until after the Android version is released. Although, before then, I expect the ad-supported free version for iOS to become available.

2014-04-14

Released!

This post is a little delayed, but I finally released Zoing to the App Store about two weeks ago! It's available in all countries. Please check it out and write a nice review, 5 stars would also be great.

I also just submitted the free, ad-supported version to the App Store two days ago. Apple typically takes about 10 days to review new apps, so I'll announce it here when that's available. It's exactly the same game, but shows ads on some of the screens, although not on the main game screens.