tag:blogger.com,1999:blog-51816037516265676792024-03-13T05:15:39.935-07:00Chasing TailSoftware Engineering thoughtsKopperhttp://www.blogger.com/profile/01303976100974534580noreply@blogger.comBlogger6125tag:blogger.com,1999:blog-5181603751626567679.post-21154779307599230232009-02-16T00:42:00.000-08:002009-02-16T11:15:55.357-08:00Configuring NUnit tests to work with log4net<strong><span style="font-family:verdana;">The problem</span></strong><p><span style="font-family:verdana;">I would like my NUnit tests to produce log4net logs.</span></p><strong><span style="font-family:verdana;">Setting (my context)</span></strong><p><span style="font-family:verdana;">I have all my unit tests in separate assembly and I use NUnit GUI to execute the tests.</span></p><strong><span style="font-family:verdana;">The Solution</span></strong><p><span style="font-family:verdana;">Existing solutions</p><p><a href="http://www.softwarefrontier.com/2007/09/using-log4net-with-nunit.html">Use assembly attribute to declare log4net configuration</span></a><span style="font-family:verdana;"><a href="http://tdoks.blogspot.com/2007/12/logging-with-log4net-and-nunit.html"><br />Load log4net configuration for each namespace using NUnit's SetUpFixture attribute</a></span></p><span style="font-family:verdana;">Solution I use</span><ol><li><span style="font-family:verdana;">Create app.config file containing log4net configuration.</span></li><li><span style="font-family:verdana;">Create custom TestCase class which loads log4net configuration before running tests.</span></li><li><span style="font-family:verdana;">Inherit each test class from above TestCase class.</span></li></ol><strong><span style="font-family:verdana;">Potential problems</span></strong><ul><li><span style="font-family:verdana;">wrong config file can be used (other than "our" app.config with log4net configuration),</span></li><li><span style="font-family:verdana;">log4net configuration can be not loaded before the tests,</span></li><li><span style="font-family:verdana;">wrong log4net configuration can be loaded.</span></li></ul><strong><span style="font-family:verdana;">Tests for above problems</span></strong><ol><li><span style="font-family:verdana;">Test for config file</span><p><span style="font-family:verdana;">First add "special" key to appSettings in app.config. Value from this key will be used to check if expected config file was loaded.</span></p><pre class="xml" name="code"><br /><appSettings><br /> <add key="config.type" value="unit.test"/><br /></appSettings><br /></pre><span style="font-family:verdana;">Second - add test which will check for existence of this "special" key-value.</span><pre class="c-sharp" name="code"><br />[Test]<br />public void AppConfigForTestIsUsed()<br />{<br /> Assert.AreEqual("unit.test", ConfigurationManager.AppSettings["config.type"]);<br />}</pre><span style="font-family:verdana;">REMARK: Please use ConfigurationManager instead of ConfigurationSettings, the latter is deprecated.<br /><br /></li></span><li><span style="font-family:verdana;">Test for log4net config file</span><p><span style="font-family:verdana;">Check if at least one of defined appenders was loaded.</span></p><pre class="c-sharp" name="code"><br />[Test]<br />public void Log4NetConfigurationLoaded()<br />{<br /> IAppender[] appenders = LogManager.GetRepository().GetAppenders();<br /> ICollection appenderNames = appenders.Select(appender => appender.Name).ToArray();<br /> Assert.Contains("NUnitFileAppender", appenderNames);<br />}</pre></li></ol><strong><span style="font-family:verdana;">Implementation</span></strong><ol><li><span style="font-family:verdana;">app.config file</span><pre class="xml" name="code"><br /><?xml version="1.0" encoding="utf-8" ?><br /><configuration><br /> <configSections><br /> <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" /><br /> </configSections><br /> <appSettings><br /> <add key="config.type" value="unit.test"/><br /> </appSettings><br /> <log4net><br /> <appender name="NUnitFileAppender" type="log4net.Appender.FileAppender"><br /> <file value="log4net.log" /><br /> <appendToFile value="false" /><br /> <layout type="log4net.Layout.PatternLayout"><br /> <conversionPattern value="%date %thread %-5level %logger{1} - %message%newline" /><br /> </layout><br /> </appender><br /> <appender name="NUnitConsoleAppender" type="log4net.Appender.ConsoleAppender"><br /> <layout type="log4net.Layout.PatternLayout"><br /> <conversionPattern value="%date %thread %-5level %logger{1} - %message%newline" /><br /> </layout><br /> </appender><br /> <root><br /> <level value="DEBUG" /><br /> <appender-ref ref="NUnitFileAppender" /><br /> <appender-ref ref="NUnitConsoleAppender" /><br /> </root><br /> </log4net><br /></configuration></pre><li><span style="font-family:verdana;">Custom TestCase class</span><pre class="c-sharp" name="code"><br />public class TestCaseWithLog4NetSupport : Mockery<br />{<br /> [TestFixtureSetUp]<br /> public void ConfigureLog4Net()<br /> {<br /> XmlConfigurator.Configure();<br /> }<br />}</pre><span style="font-family:verdana;">Above solution causes that log4net configuration is loaded before each TestFixture class.</span><p><span style="font-family:verdana;">REMARK: Becuase I want to use NMock2 my TestCase class extends Mockery class.</span></p></li><li><span style="font-family:verdana;">Actual test</span><pre class="c-sharp" name="code"><br />[TestFixture]<br />public class BusinessLogicTest : TestCaseWithLog4NetSupport<br />{</pre></li></ol><p><span style="font-family:verdana;">That's it :-)</span></p>Kopperhttp://www.blogger.com/profile/01303976100974534580noreply@blogger.com0tag:blogger.com,1999:blog-5181603751626567679.post-47971806278547225342008-12-06T03:48:00.000-08:002008-12-06T04:05:27.648-08:00Pesticide paradox<span style="font-family:georgia;"><span style="font-size:100%;">Do you have a lot of automatic tests? Do you continously add new test cases and improve existing ones? No? You shouldn't sleep well... ;-)</span></span><span style="font-family:georgia;"><span style="font-size:100%;"><br /></span></span><span style="font-family:georgia;"><span style="font-size:100%;"><br /></span></span><span style="font-family:georgia;"><span style="font-size:100%;">Please be aware that the value of the same tests executed over and over again (e.g. regression tests) decreases with time. This is known as "pesticide paradox".</span></span><span style="font-family:georgia;"><span style="font-size:100%;"><br /></span></span><span style="font-family:georgia;"><span style="font-size:100%;"><br /></span></span><span style="font-family:georgia;"><span style="font-size:100%;">Following ISTQB "Certified T</span></span><span style="font-family:georgia;"><span style="font-size:100%;">ester Foundati</span></span><span style="font-family:georgia;"><span style="font-size:100%;">on Level Syllabus":</span></span><span style="font-family:georgia;"><span style="font-size:100%;"><br /></span></span><p><span style="font-size:100%;"><BLOCKQUOTE style="font-family:times new roman;">If the same tests are repeated over and over again, eventually the same set of test cases will no longer find any new defects. To overcome this “pesticide paradox”, the test cases need to be regularly reviewed and revised, and new and different tests need to be written to exercise different parts of the software or system to potentially find more defects.</BLOCKQUOTE></span></p><p><span style="font-family:georgia;"><span style="font-size:100%;"><strong>The conclusion:</strong></span></span><span style="font-family:georgia;"><span style="font-size:100%;"><br /></span></span></p><p><span style="font-family:georgia;"><span style="font-size:100%;">It's not enough to have automatic tests suite. It's essential to improve it over the time.</span></span></p>Kopperhttp://www.blogger.com/profile/01303976100974534580noreply@blogger.com0tag:blogger.com,1999:blog-5181603751626567679.post-53428399419518465302008-11-23T13:36:00.000-08:002009-02-16T14:26:43.555-08:00C# needs Linq to be (really) functional language<p><span style="font-family:verdana;">C# is a functional language. Although this is true for for all versions of the language there is very significant difference in usability between C# 3.0 and older releases. </span></p><p><span style="font-family:verdana;">To see some differences let's compare the same code fragment </span><span style="font-family:verdana;">using some typical functional constructs </span><span style="font-family:verdana;">written in both: C# 2.0 and 3.0. First let's write simple function, which inspects (converts to readable string) array content using C# 2.0.</span></p><pre class="c-sharp" name="code"><br />public class Arrays<br />{<br /> public static string Inspect<T>(T obj)<br /> {<br /> return obj != null ? obj.ToString() : "null";<br /> }<br /> public static string Inspect<T>(T[] array)<br /> {<br /> Converter<T, string> map = new Converter<T, string>(Inspect);<br /> string[] converted = Array.ConvertAll<T, string>(array, map);<br /> return "[" + String.Join(", ", converted) + "]";<br /> }<br />}</pre><span style="font-family:verdana;">Above example illustrates that functional programming with C# may be very hard to read, verbose and... <strong><span style="font-size:130%;">so ugly!</span></strong></span><p><span style="font-family:verdana;">Let see the difference when using C# 3.0, which introduces a lot of new features, inlcuding:</span></p><ul><li><span style="font-family:verdana;">Lambda Expressions (you can easily define functions "in-line"),</span></li><li><span style="font-family:verdana;">Extension Methods (you can use bunch of helper methods added to existing types or you can add your own methods to any type, including System types),</span></li><li><span style="font-family:verdana;">Type Inference For Generics (in most cases you don't have to write types in <> brackets when invoking generic method),</span></li><li><span style="font-family:verdana;">Implicitly Typed Local Variables (you don't have to declare types for local variables),</span></li><li><span style="font-family:verdana;">Query Expressions (the most valuable and unique? feature introduced with LINQ, you can write select-from-where queries directly in C# language for different data sources inlcuding relational databases and XML files).</span></li></ul><br /><span style="font-family:verdana;">The same function now written in C# 3.0 is... different ;-)</span><pre class="c-sharp" name="code"><br />using System.Linq;<br /><br />public class Arrays<br />{<br /> public static string Inspect<T>(T[] array)<br /> {<br /> var converted = array.Select(obj => obj != null ? obj.ToString() : "null").ToArray();<br /> return "[" + String.Join(", ", converted) + "]";<br /> }<br />}</pre><span style="font-family:verdana;">Let's see in details two key code lines...</span><pre class="c-sharp" name="code"><br />using System.Linq;<br /></pre><span style="font-family:verdana;">Above line is crucial for the example, because without using Linq we wouldn't be able to use bunch of useful extensions methods provided by Linq for IEnumerable interface (in our case method Select wouldn't be visible to the compiler). </span><span style="font-family:verdana;">Here is one "trick" I wasted some time to find out and make this working. When, after adding "using System.Linq", you see following error:</span><p><span style="font-size:85%;"><span style="font-family:courier new;">The type or namespace name 'Linq' does not exist in the<br />namespace 'System' (are you missing an assembly reference?)</span></span><br /></p><p><span style="font-family:verdana;">then this is most probably caused by missing reference to assembly containing Linq. This is quite obvious(?), but the name of asssembly containing Linq is not. It is <strong>System.Core</strong>.</span></p><span style="font-family:verdana;">The second line although not very pretty looks quite innocent, doesn't it?</span><pre class="c-sharp" name="code"><br />var converted = array.Select(obj => obj != null ? obj.ToString() : "null").ToArray();<br /></pre><span style="font-family:verdana;">Let's see key (new) elements related to C# 3.0:<ol><li>We don't have to declare type for local variables, so in our example we can use<pre class="c-sharp" name="code"><br />var converted =<br /></pre>instead of<pre class="c-sharp" name="code"><br />string[] converted =<br /></pre></li><li>We can use extension methods for IEnumerable interface provided by Linq directly on IEnumerable instance. In our case we can use<pre class="c-sharp" name="code"><br /> array.Select<br /></pre>instead of<pre class="c-sharp" name="code"><br /> Array.ConvertAll<br /></pre></li><li>We can use type inference for invoking generic methods, so we don't have to write types in <> brackets. We can simply type:<pre class="c-sharp" name="code"><br /> Select( ToArray(<br /></pre>instead of<pre class="c-sharp" name="code"><br /> Select<T, string>( ToArray<string>(<br /></pre></li><li>We can use lambda expressions instead of declaring separate methods or delegates.<pre class="c-sharp" name="code"><br /> obj => obj != null ? obj.ToString() : "null"<br /></pre>instead of<pre class="c-sharp" name="code"><br />public static string Inspect<T>(T obj)<br />{<br /> return obj != null ? obj.ToString() : "null";<br />}<br /></pre></li></ol>Unfortunatelly there are things we cannot do, although we would expect to be able to do them. One of the examples is, that we cannot assign lambda expression to "untyped" variable. In our example we cannot write:<pre class="c-sharp" name="code"><br />var converter = obj => obj != null ? obj.ToString() : "null";<br />var converted = array.Select(converter).ToArray();<br /></pre>to compile, we would have to change this into<pre class="c-sharp" name="code"><br />Func<T, string> converter = obj => obj != null ? obj.ToString() : "null";<br />var converted = array.Select(converter).ToArray();<br /></pre>that's why it's better to put this lambda expression inline, even for the cost of long and harder to read line.</span><p><span style="font-family:verdana;">As we can see C# 3.0 gives us (especially when using Linq) powerfull language features, that make the language more friendly for functional programming. Although new features are not perfect, they are indeed useful.</span></p>Kopperhttp://www.blogger.com/profile/01303976100974534580noreply@blogger.com0tag:blogger.com,1999:blog-5181603751626567679.post-40010322950595741322008-04-29T13:09:00.000-07:002008-04-29T13:48:31.554-07:00Books about Software Engineering I recommendMy favorite book about Agile:<br /><a href="http://www.amazon.com/Agility-Discipline-Made-Easy-Addison-Wesley/dp/0321321308">Agility and Discipline Made Easy</a> by Perr Kroll and Bruce MacIsaac (no polish edition)<br /><br />Another great book about Agile:<br /><a href="http://www.amazon.com/Lean-Software-Development-Agile-Toolkit/dp/0321150783">Lean Software Development</a> by Mary and Tom Poppendiecks (no polish edition)<br /><br />Introduction to XP (short and nice, a lot of good practices):<br /><a href="http://www.amazon.com/Extreme-Programming-Explained-Embrace-Change/dp/0321278658/ref=pd_bbs_sr_1?ie=UTF8&s=books&qid=1209500371&sr=1-1">Extreme Programming Explained</a> by Kent Beck and Cynthia Andres (<a href="http://mikom.pwn.pl/4869_pozycja.html">polish edition</a>)<br /><br />Very good book about one of the most important "best practices":<br /><a href="http://www.amazon.com/Continuous-Integration-Improving-Addison-Wesley-Signature/dp/0321336380/ref=pd_bbs_sr_1?ie=UTF8&s=books&qid=1209500511&sr=1-1">Continuous Integration</a> by Paul Duvall (no polish edition)<br /><br />You cannot do "real" refactoring without reading it first:<br /><a href="http://www.amazon.com/Refactoring-Improving-Existing-Addison-Wesley-Technology/dp/0201485672/ref=pd_bbs_sr_1?ie=UTF8&s=books&qid=1209500646&sr=1-1">Refactoring</a> by Marting Fowler (<a href="http://www.wnt.com.pl/product.php?action=0&prod_id=704&hot=1">polish edition</a>)<br /><br />Two not perfect, but very valuable books about data:<br /><a href="http://www.amazon.com/First-Course-Database-Systems-GOAL/dp/013600637X/ref=sr_1_2?ie=UTF8&s=books&qid=1209500763&sr=1-2">First Course in Database Systems</a> by Jeffrey Ullman and Jenninfer Widom (<a href="http://www.wnt.com.pl/product.php?action=0&prod_id=225&hot=1">polish edition</a>)<br /><a href="http://www.amazon.com/Database-Systems-Implementation-Management-International/dp/0201342871/ref=sr_1_1?ie=UTF8&s=books&qid=1209500834&sr=1-1">Database Systems</a> by Thomas Connolly and Carolyn Begg (<a href="http://www.readme.pl/x_C_I__P_10840-110003.html">polish edition</a>)<br /><br />Very nice book about combining business and technical point of view:<br /><a href="http://www.amazon.com/Beyond-Software-Architecture-Sustaining-Addison-Wesley/dp/0201775948/ref=sr_1_2?ie=UTF8&s=books&qid=1209500969&sr=1-2">Beyond Software Architecture</a> by Luke Hohmann (<a href="http://helion.pl/ksiazki/moreao.htm">polish edition</a>)<br /><br />Two boring, but very valuable (necessary!) books providing an overview on Software Engineering:<br /><a href="http://www.amazon.com/Software-Engineering-Practitioners-Roger-Pressman/dp/007301933X/ref=pd_bbs_sr_1?ie=UTF8&s=books&qid=1209501088&sr=1-1">Software Engineering</a> by Roger Pressman (<a href="http://www.wnt.pl/product.php?action=0&prod_id=531&hot=1">polish edition</a>)<br /><a href="http://www.amazon.com/Software-Engineering-International-Computer-Science/dp/0321313798/ref=pd_bbs_sr_1?ie=UTF8&s=books&qid=1209501182&sr=1-1">Software Engineering</a> by Ian Sommerville (<a href="http://www.wnt.pl/product.php?action=0&prod_id=404&hot=1">polish edition</a>)<br /><br />Want to borrow in Krakow? Don't hesitate to contact me at <a href="mailto:AdamCzepil@gmail.com">AdamCzepil@gmail.com</a>.Kopperhttp://www.blogger.com/profile/01303976100974534580noreply@blogger.com0tag:blogger.com,1999:blog-5181603751626567679.post-51762302069019495782008-03-07T15:29:00.000-08:002009-02-16T14:14:19.086-08:00Pair Programming in Practice<p><b>Is pair programming a viable method?</b></p><p><b>Yes</b>. This is <b>a viable method</b>. In our small (8 people) company we are using pair programming almost every day. <b>But...</b> it is not so easy as I thought at the beginning.</p>1. I think it is <b>almost impossible to work in pair whole day</b> (I cannot explain this very precisely, but when I work in pair I also need some loneliness from time to time, just to take a breath). For me 6h is max.<br />2. <b>Working in pair is very tiring.</b> After whole day of pair programming I am usually not in the mood for parties ;-) I just want to go to sleep.<br />3. <b>Not always two people make good pair</b> for particular task. It can be a problem for me to say "no" to colleague from the team if I don't feel we are right pair for the task.<br />4. I <b>need several days to get used to new colleague.</b> Usually, at the beginning, mostly one person writes the code and we switch rarely. We need some time to learn how to switch roles often and in the way, that we are "equal" parts of pair (i.e. 50% of time coding, 50% of time helping).<br />5. <b>Some tasks</b> I <b>prefer to do alone.</b> This includes very simple, but arduous tasks (it's faster then), but also tasks I know I am the best expert in the team (then I have more fun because I don't have to answer all those questions :)<br />6. It is a <b>problem </b>for me <b>to find balance between being a teacher and student.</b> In theory in the second case above I should work in pair with my colleagues to teach them ("exchange knowledge"). And I do. My colleagues also do this with me. But not always, it's very hard to be a teacher or even a student all the time. I think we usually avoid situations "teacher-student". They are rather exception, than a rule. But we keep in mind, that... "today you teach me A, tomorrow I'll teach you B".<br /><br />Some benefits:<br /><br />1. <b>Knowlegde exchange is unbelievable. </b>Even though we don't work in pairs all the time. Even though we don't like teach/learn all the time.<br />2. I believe <b>hard/complex tasks are done faster in pair</b> vs. single.<br />3. I believe, that the <b>code we produce in pairs is better</b>, especially in terms of maintainability.<br />4. <b>Two heads</b> sometimes make up amazing, surprisingly <b>good solutions</b> (creativity!)<br />5. I <b>like it</b>, I <b>learn a lot </b>from other guys (and I hope vice-versa :)<br /><br />Some problems I know:<br /><br />1. It is very <b>hard to prove, that working in pairs is better</b> than single work. There is several research papers about it, bit the conclusions are contradictory, or at least - "fuzzy".<br />2. I think that in usual case (task not very complex) this is simply <b>not true, that pair programming is more effective.</b> I belive, that is better from other reasons (better code, knowledge sharing and fun).<br /><br />In our city several compannies claims to use pair programming in some projects. This includes Sabre (which is Agile-based, so nothing surprising) and... Motorola! AFAIK it is still very, <b>very rare practice.</b>Kopperhttp://www.blogger.com/profile/01303976100974534580noreply@blogger.com1tag:blogger.com,1999:blog-5181603751626567679.post-87865027417828975692008-02-24T10:34:00.000-08:002008-02-24T13:53:11.286-08:00Deep equality in modern languagesI was recently creating with Ruby simple <a href="http://en.wikipedia.org/wiki/Extract_transform_load">Extract-Transform-Load</a> tool for importing data set from XML file to our databse. At the beginning it seemed very easy, although painful task. The first problem I was trying to solve was testing. Nothing interesting, I thought. And I was almost right. Almost...<br /><br />Testing algorithm was very simple:<br /><br />1. expected = predefined data set (so called test fixture)<br />2. import data from XML file to DB<br />3. actual = load imported data from DB<br />4. assert_equal expected, actual, "all data should be equal"<br /><br />The problem was in assert_equal, because "default" equal was not I wanted to be. <strong>Default equal is shallow</strong>, i.e. compares only objects itself without its dependencies.<strong> I needed deep equality.</strong><br /><br />Example 1<br /><span style="font-family:courier new;"><br /><span><span><span><span><span><span>title_a = ['Some title', author_a]<br />author_a = ['Gauss', 'C']</span></span></span></span></span></span></span><br /><br /><br />In this example title and author are in relation "has a" - title_a has author_a. This causes "standard" equal to not work correctly. Let for example<br /><br /><span><span><span><span><span><span>author_b = ['Gauss', 'C'].</span></span></span></span></span></span><br /><br />Authors a and b are equal, but have different identifiers (identities, references), thus if we define<br /><br /><span><span><span><span><span><span><span style="font-family:courier new;">title_b = ['Some title', author_b]</span></span></span></span></span></span></span><br /><br />we'll get title_a not equal title_b. To obtain expected equality result I must redefine default equality operator. So far, so obvious...<br /><br />The <strong>problem</strong> was, that I had to <strong>redefine</strong> this <strong>operator</strong><strong> in all</strong> data model <strong>classes</strong>. Ordinary thing, I thought. I was always doing this way. This is very simple, I have to choose which "fields" compare directly and which "through references". Very simple. And stupid. And<strong> error prone</strong>. And time consuming. And very<strong> hard to maintain</strong>. Why, the hell, languages I am working with do not provide such obvious functionality!? Maybe I missed something? Maybe everyone knows how to do this very easily, except me?<br /><br />I did small <strong>research</strong> and it appeared that such <strong>functionality</strong> is <strong>implemented</strong> in... <strong>Eiffel</strong>, but only partailly [1]. Java, C++, Ruby and even Smalltalk don't have such language feature.<br /><br />Maybe it is hard? I thought. My<strong> first idea</strong> was to <strong>compare</strong> two directed <strong>graphs</strong> created from object instances and (some*) references between them. The problem is well known, it is called <a href="http://en.wikipedia.org/wiki/Graph_isomorphism">graph isomorphism</a>. Quickly it appeared, that graph isomorphism (GI) problem has no polynomial solution. Curiously, <a href="http://mathworld.wolfram.com/IsomorphicGraphs.html">it is believed</a> that GI is neither P nor NP-complete... There is special complexity class, called GI-complete, for this problem and all problems with polynomial-time Turing reduction. It is belived, although obviously (P=NP?) not proven, that:<br /><br /><div align="center">P-complete < GI-complete < NP-complete <br /></div><br />Anyway it seemed, that although <strong>solution</strong> is <strong>possible</strong>, it may be <strong>not</strong> <strong>efficient</strong>... <br /><br />* - more about this in my next post <br /><br />My <strong>second idea</strong> was that, comparing to GI problem, we have additional information - "root" vertices we are starting comparison from, and thus the solution may be more efficient than for GI problem. After some googling I found I was right. I found very interesting paper [2] which, besides formal point of view on deep equality, defines <strong>polynomial algorithm </strong>for the problem, even in the case with circular references! Additionally, algorithm looks quite simple to implement. <br /><br />My third idea was, that the solution may be even simpler if we assume no circular references. I don't know if this is true, but I would like to check this, because my... <br /><br />Fourth, <strong>final idea</strong>, was to<strong> implement deep equality</strong> solution for <strong>Ruby</strong>. But <strong>before </strong>I'll do this <strong>I will write next post</strong> entitled <strong>"Deep equality and Aggregation"</strong> i.a. describing how to declare which relations should be taken into consideration by deep_equal operation.<br /><br />References:<br /><br />1. <a href="http://citeseer.ist.psu.edu/grogono00copying.html">Copying and Comparing: Problems and Solutions</a>, P. Grogono &<br /> M. Sakkinen, 2000<br />2. <a href="http://citeseer.ist.psu.edu/420753.html">Deep Equality Revisited</a>, S. Abiteboul & J.V. den Bussche, 1995Kopperhttp://www.blogger.com/profile/01303976100974534580noreply@blogger.com1