Patchwork [RFC,v3] scmutil: add a simple key-value file helper

login
register
mail settings
Submitter Kostia Balytskyi
Date Dec. 5, 2016, 11:03 p.m.
Message ID <bdc49209b0b926214e86.1480979015@dev1902.lla1.facebook.com>
Download mbox | patch
Permalink /patch/17823/
State Superseded
Headers show

Comments

Kostia Balytskyi - Dec. 5, 2016, 11:03 p.m.
# HG changeset patch
# User Kostia Balytskyi <ikostia@fb.com>
# Date 1480978778 28800
#      Mon Dec 05 14:59:38 2016 -0800
# Node ID bdc49209b0b926214e865f9a98ea987866f64d68
# Parent  243ecbd4f5c9f452275d4435866359cf84dc03ff
scmutil: add a simple key-value file helper

The purpose of the added class is to serve purposes like save files of shelve
or state files of shelve, rebase and histedit. Keys of these files can be
alphanumeric and start with letters, while values must not contain newlines.
Keys which start with an uppercase letter are required, while other keys
are optional.

In light of Mercurial's reluctancy to use Python's json module, this tries
to provide a reasonable alternative for a non-nested named data.
Comparing to current approach of storing state in plain text files, where
semantic meaning of lines of text is only determined by their oreder,
simple key-value file allows for reordering lines and thus helps handle
optional values.

Initial use-case I see for this is obs-shelve's shelve files. Later we
can possibly migrate state files to this approach.

The test is in a new file beause I did not figure out where to put it
within existing test suite. If you give me a better idea, I will gladly
follow it.
Yuya Nishihara - Dec. 6, 2016, 1:04 p.m.
On Mon, 5 Dec 2016 15:03:35 -0800, Kostia Balytskyi wrote:
> # HG changeset patch
> # User Kostia Balytskyi <ikostia@fb.com>
> # Date 1480978778 28800
> #      Mon Dec 05 14:59:38 2016 -0800
> # Node ID bdc49209b0b926214e865f9a98ea987866f64d68
> # Parent  243ecbd4f5c9f452275d4435866359cf84dc03ff
> scmutil: add a simple key-value file helper
> 
> The purpose of the added class is to serve purposes like save files of shelve
> or state files of shelve, rebase and histedit. Keys of these files can be
> alphanumeric and start with letters, while values must not contain newlines.
> Keys which start with an uppercase letter are required, while other keys
> are optional.

> +class simplekeyvaluefile(object):
> +    """A simple file with key=value lines
> +
> +    Keys must be alphanumerics and start with a letter, values might not
> +    contain '\n' characters
> +    """
> +    class InvalidKeyValueFile(Exception): pass
> +    class InvalidKeyInFileException(Exception): pass
> +    class InvalidValueInFileException(Exception): pass
> +    class MissingRequiredKeyInFileException(Exception): pass
> +
> +    # if KEYS is non-empty, read values are validated against it:
> +    # each key is a tuple (keyname, required)
> +    KEYS = []

If we want to define a required key back to older Mercurial versions (like
mergestate), we'll need a static rule such as "uppercase is required" seen in
your V1 patch.

> +    def __init__(self, vfs, path):
> +        self.vfs = vfs
> +        self.path = path
> +
> +    def validate(self, d):
> +        for key, req in self.KEYS:
> +            if req and key not in d:
> +                e = _("missing a required key: '%s'") % key
> +                raise self.MissingRequiredKeyInFileException(e)

_() is necessary only for user-facing errors.

I don't think nested classes are useful unless we want to give a room for
sub classes to override them.

> +            lines.append("%s=%s\n" % (k, v))
> +        with self.vfs(self.path, mode='wb', atomictemp=True) as fp:
> +            fp.write(''.join(lines))

BTW, the syntax of this file seems somewhat similar to config.config.
Kostia Balytskyi - Dec. 6, 2016, 1:22 p.m.
On 12/6/16 1:04 PM, Yuya Nishihara wrote:
> On Mon, 5 Dec 2016 15:03:35 -0800, Kostia Balytskyi wrote:

>> # HG changeset patch

>> # User Kostia Balytskyi <ikostia@fb.com>

>> # Date 1480978778 28800

>> #      Mon Dec 05 14:59:38 2016 -0800

>> # Node ID bdc49209b0b926214e865f9a98ea987866f64d68

>> # Parent  243ecbd4f5c9f452275d4435866359cf84dc03ff

>> scmutil: add a simple key-value file helper

>>

>> The purpose of the added class is to serve purposes like save files of shelve

>> or state files of shelve, rebase and histedit. Keys of these files can be

>> alphanumeric and start with letters, while values must not contain newlines.

>> Keys which start with an uppercase letter are required, while other keys

>> are optional.

>> +class simplekeyvaluefile(object):

>> +    """A simple file with key=value lines

>> +

>> +    Keys must be alphanumerics and start with a letter, values might not

>> +    contain '\n' characters

>> +    """

>> +    class InvalidKeyValueFile(Exception): pass

>> +    class InvalidKeyInFileException(Exception): pass

>> +    class InvalidValueInFileException(Exception): pass

>> +    class MissingRequiredKeyInFileException(Exception): pass

>> +

>> +    # if KEYS is non-empty, read values are validated against it:

>> +    # each key is a tuple (keyname, required)

>> +    KEYS = []

> If we want to define a required key back to older Mercurial versions (like

> mergestate), we'll need a static rule such as "uppercase is required" seen in

> your V1 patch.

I am not sure I understand this one. If you meant that 
simplekeyvaluefile should be able
to read state files created by older mercurail versions, I don't think 
it's possible. I don't think
simplekeyvaluefile itself should provide a backwards-compatible layer. 
Rather, when we
migrate a state file, we should first try to parse it into a 
simlpekeyvaluefile and if this fails,
fallback to a traditional file format. New fields should only be added 
to a simplekeyvaluefile
code path, while traditional parsing should replace them with some sort 
of empty/default values.

Can you clarify what you meant by "define a required key back to older 
Mercurial versions"?
>

>> +    def __init__(self, vfs, path):

>> +        self.vfs = vfs

>> +        self.path = path

>> +

>> +    def validate(self, d):

>> +        for key, req in self.KEYS:

>> +            if req and key not in d:

>> +                e = _("missing a required key: '%s'") % key

>> +                raise self.MissingRequiredKeyInFileException(e)

> _() is necessary only for user-facing errors.

>

> I don't think nested classes are useful unless we want to give a room for

> sub classes to override them.

>

>> +            lines.append("%s=%s\n" % (k, v))

>> +        with self.vfs(self.path, mode='wb', atomictemp=True) as fp:

>> +            fp.write(''.join(lines))

> BTW, the syntax of this file seems somewhat similar to config.config.

I wanted something explicitly much simpler than config.config (also with 
possibility to further limit
what can be values if needed).
> _______________________________________________

> Mercurial-devel mailing list

> Mercurial-devel@mercurial-scm.org

> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Jun Wu - Dec. 6, 2016, 1:28 p.m.
Excerpts from Kostia Balytskyi's message of 2016-12-06 13:22:40 +0000:
> On 12/6/16 1:04 PM, Yuya Nishihara wrote:
> > On Mon, 5 Dec 2016 15:03:35 -0800, Kostia Balytskyi wrote:
> >> # HG changeset patch
> >> # User Kostia Balytskyi <ikostia@fb.com>
> >> # Date 1480978778 28800
> >> #      Mon Dec 05 14:59:38 2016 -0800
> >> # Node ID bdc49209b0b926214e865f9a98ea987866f64d68
> >> # Parent  243ecbd4f5c9f452275d4435866359cf84dc03ff
> >> scmutil: add a simple key-value file helper
> >>
> >> The purpose of the added class is to serve purposes like save files of shelve
> >> or state files of shelve, rebase and histedit. Keys of these files can be
> >> alphanumeric and start with letters, while values must not contain newlines.
> >> Keys which start with an uppercase letter are required, while other keys
> >> are optional.
> >> +class simplekeyvaluefile(object):
> >> +    """A simple file with key=value lines
> >> +
> >> +    Keys must be alphanumerics and start with a letter, values might not
> >> +    contain '\n' characters
> >> +    """
> >> +    class InvalidKeyValueFile(Exception): pass
> >> +    class InvalidKeyInFileException(Exception): pass
> >> +    class InvalidValueInFileException(Exception): pass
> >> +    class MissingRequiredKeyInFileException(Exception): pass
> >> +
> >> +    # if KEYS is non-empty, read values are validated against it:
> >> +    # each key is a tuple (keyname, required)
> >> +    KEYS = []
> > If we want to define a required key back to older Mercurial versions (like
> > mergestate), we'll need a static rule such as "uppercase is required" seen in
> > your V1 patch.
> I am not sure I understand this one. If you meant that 
> simplekeyvaluefile should be able
> to read state files created by older mercurail versions, I don't think 
> it's possible. I don't think
> simplekeyvaluefile itself should provide a backwards-compatible layer. 
> Rather, when we
> migrate a state file, we should first try to parse it into a 
> simlpekeyvaluefile and if this fails,
> fallback to a traditional file format. New fields should only be added 
> to a simplekeyvaluefile
> code path, while traditional parsing should replace them with some sort 
> of empty/default values.
> 
> Can you clarify what you meant by "define a required key back to older 
> Mercurial versions"?

It means, for example, in hg v5.0, we have keys "a" and "b" as "required".
And we add another key "c" as required in v5.1. What will v5.1 do with the
simplekeyvaluefile written by v5.0 ? (and what will we do if we want to
remove required keys?)

> >
> >> +    def __init__(self, vfs, path):
> >> +        self.vfs = vfs
> >> +        self.path = path
> >> +
> >> +    def validate(self, d):
> >> +        for key, req in self.KEYS:
> >> +            if req and key not in d:
> >> +                e = _("missing a required key: '%s'") % key
> >> +                raise self.MissingRequiredKeyInFileException(e)
> > _() is necessary only for user-facing errors.
> >
> > I don't think nested classes are useful unless we want to give a room for
> > sub classes to override them.
> >
> >> +            lines.append("%s=%s\n" % (k, v))
> >> +        with self.vfs(self.path, mode='wb', atomictemp=True) as fp:
> >> +            fp.write(''.join(lines))
> > BTW, the syntax of this file seems somewhat similar to config.config.
> I wanted something explicitly much simpler than config.config (also with 
> possibility to further limit
> what can be values if needed).
> > https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel 
>
Kostia Balytskyi - Dec. 6, 2016, 1:39 p.m.
On 12/6/16 1:28 PM, Jun Wu wrote:
> Excerpts from Kostia Balytskyi's message of 2016-12-06 13:22:40 +0000:

>> On 12/6/16 1:04 PM, Yuya Nishihara wrote:

>>> On Mon, 5 Dec 2016 15:03:35 -0800, Kostia Balytskyi wrote:

>>>> # HG changeset patch

>>>> # User Kostia Balytskyi <ikostia@fb.com>

>>>> # Date 1480978778 28800

>>>> #      Mon Dec 05 14:59:38 2016 -0800

>>>> # Node ID bdc49209b0b926214e865f9a98ea987866f64d68

>>>> # Parent  243ecbd4f5c9f452275d4435866359cf84dc03ff

>>>> scmutil: add a simple key-value file helper

>>>>

>>>> The purpose of the added class is to serve purposes like save files of shelve

>>>> or state files of shelve, rebase and histedit. Keys of these files can be

>>>> alphanumeric and start with letters, while values must not contain newlines.

>>>> Keys which start with an uppercase letter are required, while other keys

>>>> are optional.

>>>> +class simplekeyvaluefile(object):

>>>> +    """A simple file with key=value lines

>>>> +

>>>> +    Keys must be alphanumerics and start with a letter, values might not

>>>> +    contain '\n' characters

>>>> +    """

>>>> +    class InvalidKeyValueFile(Exception): pass

>>>> +    class InvalidKeyInFileException(Exception): pass

>>>> +    class InvalidValueInFileException(Exception): pass

>>>> +    class MissingRequiredKeyInFileException(Exception): pass

>>>> +

>>>> +    # if KEYS is non-empty, read values are validated against it:

>>>> +    # each key is a tuple (keyname, required)

>>>> +    KEYS = []

>>> If we want to define a required key back to older Mercurial versions (like

>>> mergestate), we'll need a static rule such as "uppercase is required" seen in

>>> your V1 patch.

>> I am not sure I understand this one. If you meant that

>> simplekeyvaluefile should be able

>> to read state files created by older mercurail versions, I don't think

>> it's possible. I don't think

>> simplekeyvaluefile itself should provide a backwards-compatible layer.

>> Rather, when we

>> migrate a state file, we should first try to parse it into a

>> simlpekeyvaluefile and if this fails,

>> fallback to a traditional file format. New fields should only be added

>> to a simplekeyvaluefile

>> code path, while traditional parsing should replace them with some sort

>> of empty/default values.

>>

>> Can you clarify what you meant by "define a required key back to older

>> Mercurial versions"?

> It means, for example, in hg v5.0, we have keys "a" and "b" as "required".

> And we add another key "c" as required in v5.1. What will v5.1 do with the

> simplekeyvaluefile written by v5.0 ? (and what will we do if we want to

> remove required keys?)

Hm, I don't see how upper/lower-case instead of explicit boolean flag 
handles that...
But to answer your question:
1. When you add a new field and you mark it as required, you really have 
only two options:
   a. Breaking backwards compatibility. I guess this is wanted rarely.
   b. Adding a logic to replace the missing required field with some 
default value. My opinion is that such logic should exist outside of 
simplekeyvaluefile implementation, so from this class' perspective that 
field is still optional
2. When you remove a required field or make field which was required 
optional. Because we don't limit the optional
fields in any way, this should just work - unexpected field in the file 
is just read and stored in the dictionary.
>

>>>> +    def __init__(self, vfs, path):

>>>> +        self.vfs = vfs

>>>> +        self.path = path

>>>> +

>>>> +    def validate(self, d):

>>>> +        for key, req in self.KEYS:

>>>> +            if req and key not in d:

>>>> +                e = _("missing a required key: '%s'") % key

>>>> +                raise self.MissingRequiredKeyInFileException(e)

>>> _() is necessary only for user-facing errors.

>>>

>>> I don't think nested classes are useful unless we want to give a room for

>>> sub classes to override them.

>>>

>>>> +            lines.append("%s=%s\n" % (k, v))

>>>> +        with self.vfs(self.path, mode='wb', atomictemp=True) as fp:

>>>> +            fp.write(''.join(lines))

>>> BTW, the syntax of this file seems somewhat similar to config.config.

>> I wanted something explicitly much simpler than config.config (also with

>> possibility to further limit

>> what can be values if needed).

>>> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Jun Wu - Dec. 6, 2016, 1:48 p.m.
Excerpts from Kostia Balytskyi's message of 2016-12-06 13:39:31 +0000:
> Hm, I don't see how upper/lower-case instead of explicit boolean flag 
> handles that...

I didn't realize the issue either, before Yuya's reply.

> But to answer your question:
> 1. When you add a new field and you mark it as required, you really have 
> only two options:
>    a. Breaking backwards compatibility. I guess this is wanted rarely.
>    b. Adding a logic to replace the missing required field with some 
> default value. My opinion is that such logic should exist outside of 
> simplekeyvaluefile implementation, so from this class' perspective that 
> field is still optional
> 2. When you remove a required field or make field which was required 
> optional. Because we don't limit the optional
> fields in any way, this should just work - unexpected field in the file 
> is just read and stored in the dictionary.

It seems that the user of simplekeyvaluefile have to deal with "required" /
"optional" etc.

How about just removing the "required" check in simplekeyvaluefile?
Leave them to the user to handle. That seems cleaner to me.
Kostia Balytskyi - Dec. 6, 2016, 1:54 p.m.
On 12/6/16 1:48 PM, Jun Wu wrote:
> Excerpts from Kostia Balytskyi's message of 2016-12-06 13:39:31 +0000:

>> Hm, I don't see how upper/lower-case instead of explicit boolean flag

>> handles that...

> I didn't realize the issue either, before Yuya's reply.

>

>> But to answer your question:

>> 1. When you add a new field and you mark it as required, you really have

>> only two options:

>>     a. Breaking backwards compatibility. I guess this is wanted rarely.

>>     b. Adding a logic to replace the missing required field with some

>> default value. My opinion is that such logic should exist outside of

>> simplekeyvaluefile implementation, so from this class' perspective that

>> field is still optional

>> 2. When you remove a required field or make field which was required

>> optional. Because we don't limit the optional

>> fields in any way, this should just work - unexpected field in the file

>> is just read and stored in the dictionary.

> It seems that the user of simplekeyvaluefile have to deal with "required" /

> "optional" etc.

>

> How about just removing the "required" check in simplekeyvaluefile?

> Leave them to the user to handle. That seems cleaner to me.

This is what happens if you don't inherit the class or don't override 
the empty KEYS list in the inherited class.
Jun Wu - Dec. 6, 2016, 2:08 p.m.
Excerpts from Kostia Balytskyi's message of 2016-12-06 13:54:02 +0000:
> This is what happens if you don't inherit the class or don't override 
> the empty KEYS list in the inherited class.

If KEYS is used to verify existence, how about just making it a list of
strings, instead of tuples? The "required" boolean flag seems to be
unnecessary.
Kostia Balytskyi - Dec. 6, 2016, 2:30 p.m.
On 12/6/16 2:08 PM, Jun Wu wrote:
> Excerpts from Kostia Balytskyi's message of 2016-12-06 13:54:02 +0000:

>> This is what happens if you don't inherit the class or don't override

>> the empty KEYS list in the inherited class.

> If KEYS is used to verify existence, how about just making it a list of

> strings, instead of tuples? The "required" boolean flag seems to be

> unnecessary.

That is true and I've thought about it: just store the list of required 
keys in KEYS.
The reason I want optional keys to be in this list is verbosity. I 
someone else wrote
the subclass for some key-value file, I as a maintainer have a single 
place to see all
the fields "desired" (required and optional) by current version of code. 
So the purpose
of optional keys there is documentation and readability.
Jun Wu - Dec. 6, 2016, 2:45 p.m.
Excerpts from Kostia Balytskyi's message of 2016-12-06 14:30:18 +0000:
> 
> On 12/6/16 2:08 PM, Jun Wu wrote:
> > Excerpts from Kostia Balytskyi's message of 2016-12-06 13:54:02 +0000:
> >> This is what happens if you don't inherit the class or don't override
> >> the empty KEYS list in the inherited class.
> > If KEYS is used to verify existence, how about just making it a list of
> > strings, instead of tuples? The "required" boolean flag seems to be
> > unnecessary.
> That is true and I've thought about it: just store the list of required 
> keys in KEYS.
> The reason I want optional keys to be in this list is verbosity. I 
> someone else wrote
> the subclass for some key-value file, I as a maintainer have a single 
> place to see all
> the fields "desired" (required and optional) by current version of code. 
> So the purpose
> of optional keys there is documentation and readability.

Then how about explicit "REQUIREDKEYS" and "OPTIONALKEYS"?

It seems sub-optimal to me that the "verify" method has to go through keys
unnecessarily.
Kostia Balytskyi - Dec. 6, 2016, 8:05 p.m.
On 12/6/16 2:45 PM, Jun Wu wrote:
> Excerpts from Kostia Balytskyi's message of 2016-12-06 14:30:18 +0000:

>> On 12/6/16 2:08 PM, Jun Wu wrote:

>>> Excerpts from Kostia Balytskyi's message of 2016-12-06 13:54:02 +0000:

>>>> This is what happens if you don't inherit the class or don't override

>>>> the empty KEYS list in the inherited class.

>>> If KEYS is used to verify existence, how about just making it a list of

>>> strings, instead of tuples? The "required" boolean flag seems to be

>>> unnecessary.

>> That is true and I've thought about it: just store the list of required

>> keys in KEYS.

>> The reason I want optional keys to be in this list is verbosity. I

>> someone else wrote

>> the subclass for some key-value file, I as a maintainer have a single

>> place to see all

>> the fields "desired" (required and optional) by current version of code.

>> So the purpose

>> of optional keys there is documentation and readability.

> Then how about explicit "REQUIREDKEYS" and "OPTIONALKEYS"?

>

> It seems sub-optimal to me that the "verify" method has to go through keys

> unnecessarily.

Well, I don't think performance is an issue here, these classes will 
rarely store more
than 20 fields if written by people and for people. If however someone 
uses it store
some large number of values - like some actual data - I don't think that 
KEYS is going
to be of any value in that case. I don't have a strong opinion about 
this, can change
if it matters.

Patch

diff --git a/mercurial/scmutil.py b/mercurial/scmutil.py
--- a/mercurial/scmutil.py
+++ b/mercurial/scmutil.py
@@ -1571,3 +1571,56 @@  class checkambigatclosing(closewrapbase)
     def close(self):
         self._origfh.close()
         self._checkambig()
+
+class simplekeyvaluefile(object):
+    """A simple file with key=value lines
+
+    Keys must be alphanumerics and start with a letter, values might not
+    contain '\n' characters
+    """
+    class InvalidKeyValueFile(Exception): pass
+    class InvalidKeyInFileException(Exception): pass
+    class InvalidValueInFileException(Exception): pass
+    class MissingRequiredKeyInFileException(Exception): pass
+
+    # if KEYS is non-empty, read values are validated against it:
+    # each key is a tuple (keyname, required)
+    KEYS = []
+
+    def __init__(self, vfs, path):
+        self.vfs = vfs
+        self.path = path
+
+    def validate(self, d):
+        for key, req in self.KEYS:
+            if req and key not in d:
+                e = _("missing a required key: '%s'") % key
+                raise self.MissingRequiredKeyInFileException(e)
+
+    def read(self):
+        lines = self.vfs.readlines(self.path)
+        try:
+            d = dict(line[:-1].split('=', 1) for line in lines if line)
+        except ValueError as e:
+            raise self.InvalidKeyValueFile(str(e))
+        self.validate(d)
+        return d
+
+    def write(self, data):
+        """Write key=>value mapping to a file
+        data is a dict. Keys should be alphanumerical and start with a letter.
+        Values should not contain newline characters."""
+        lines = []
+        for k, v in data.items():
+            if not k[0].isalpha():
+                e = _("keys must start with a letter in a key-value file")
+                raise self.InvalidKeyInFileException(e)
+            if not k.isalnum():
+                e = _("invalid key name in a simple key-value file")
+                raise self.InvalidKeyInFileException(e)
+            if '\n' in v:
+                e = _("invalid value in a simple key-value file")
+                raise self.InvalidValueInFileException(e)
+            lines.append("%s=%s\n" % (k, v))
+        with self.vfs(self.path, mode='wb', atomictemp=True) as fp:
+            fp.write(''.join(lines))
diff --git a/tests/test-other.t b/tests/test-other.t
new file mode 100644
--- /dev/null
+++ b/tests/test-other.t
@@ -0,0 +1,102 @@ 
+Test simple key-value files
+  $ cd $TESTTMP
+  $ hg init repo
+  $ cd $TESTTMP/repo
+
+Test simple key-value file creation
+  $ cat <<EOF > keyvalwriter.py
+  > from mercurial import ui, hg
+  > from mercurial.scmutil import simplekeyvaluefile
+  > ui = ui.ui()
+  > repo = hg.repository(ui, '$TESTTMP/repo')
+  > d = {'key1': 'value1', 'Key2': 'value2'}
+  > kvf = simplekeyvaluefile(repo.vfs, 'kvfile').write(d)
+  > EOF
+  $ python keyvalwriter.py
+  $ cat .hg/kvfile | sort
+  Key2=value2
+  key1=value1
+
+Test simple key-value file reading with invalid keys or values
+  $ cat <<EOF > keyvalwriter.py
+  > from mercurial import ui, hg
+  > from mercurial.scmutil import simplekeyvaluefile
+  > ui = ui.ui()
+  > repo = hg.repository(ui, '$TESTTMP/repo')
+  > d = {'0key1': 'value1', 'Key2': 'value2'}
+  > kvf = simplekeyvaluefile(repo.vfs, 'kvfile').write(d)
+  > EOF
+  $ python keyvalwriter.py 2>&1 | tail -1
+  mercurial.scmutil.InvalidKeyInFileException: keys must start with a letter in a key-value file
+  $ cat <<EOF > keyvalwriter.py
+  > from mercurial import ui, hg
+  > from mercurial.scmutil import simplekeyvaluefile
+  > ui = ui.ui()
+  > repo = hg.repository(ui, '$TESTTMP/repo')
+  > d = {'key@1': 'value1'}
+  > kvf = simplekeyvaluefile(repo.vfs, 'kvfile').write(d)
+  > EOF
+  $ python keyvalwriter.py 2>&1 | tail -1
+  mercurial.scmutil.InvalidKeyInFileException: invalid key name in a simple key-value file
+  $ cat <<EOF > keyvalwriter.py
+  > from mercurial import ui, hg
+  > from mercurial.scmutil import simplekeyvaluefile
+  > ui = ui.ui()
+  > repo = hg.repository(ui, '$TESTTMP/repo')
+  > d = {'key1': 'value\n1'}
+  > kvf = simplekeyvaluefile(repo.vfs, 'kvfile').write(d)
+  > EOF
+  $ python keyvalwriter.py 2>&1 | tail -1
+  mercurial.scmutil.InvalidValueInFileException: invalid value in a simple key-value file
+
+Test simple key-value file reading without field list
+  $ cat <<EOF > keyvalreader.py
+  > from mercurial import ui, hg
+  > from mercurial.scmutil import simplekeyvaluefile
+  > ui = ui.ui()
+  > repo = hg.repository(ui, '$TESTTMP/repo')
+  > d = simplekeyvaluefile(repo.vfs, 'kvfile').read()
+  > for k, v in sorted(d.items()):
+  >     print "%s => %s" % (k, v)
+  > EOF
+  $ python keyvalreader.py
+  Key2 => value2
+  key1 => value1
+
+Test simple key-value file when necessary fields are present
+  $ cat <<EOF > keyvalreader.py
+  > from mercurial import ui, hg
+  > from mercurial.scmutil import simplekeyvaluefile
+  > ui = ui.ui()
+  > repo = hg.repository(ui, '$TESTTMP/repo')
+  > class kvf(simplekeyvaluefile):
+  >     KEYS = [('key3', False), ('Key2', True)]
+  > d = kvf(repo.vfs, 'kvfile').read()
+  > for k, v in sorted(d.items()):
+  >     print "%s => %s" % (k, v)
+  > EOF
+  $ python keyvalreader.py
+  Key2 => value2
+  key1 => value1
+
+Test simple key-value file when necessary fields are absent
+  $ cat <<EOF > keyvalreader.py
+  > from mercurial import ui, hg
+  > from mercurial.scmutil import simplekeyvaluefile
+  > ui = ui.ui()
+  > repo = hg.repository(ui, '$TESTTMP/repo')
+  > class kvf(simplekeyvaluefile):
+  >     KEYS = [('Key4', True), ('Key2', True)]
+  > d = kvf(repo.vfs, 'kvfile').read()
+  > for k, v in sorted(d.items()):
+  >     print "%s => %s" % (k, v)
+  > EOF
+  $ python keyvalreader.py 2>&1 | tail -1
+  mercurial.scmutil.MissingRequiredKeyInFileException: missing a required key: 'Key4'
+
+Test invalid simple key-value file
+  $ cat <<EOF > .hg/kvfile
+  > ababagalamaga
+  > EOF
+  $ python keyvalreader.py 2>&1 | tail -1
+  mercurial.scmutil.InvalidKeyValueFile: dictionary update sequence element #0 has length 1; 2 is required