服务计算 | agenda CLI工具开发

agenda是一个用于帮助用户创建会议,并且管理会议的命令行工具。

cobra使用

Cobra既是一个用来创建强大的现代CLI命令行的golang库,也是一个生成程序应用和命令行文件的程序。此命令行工具基于cobra开发:

cobra安装

直接执行以下命令,可能安装不成功:(因为cobra用到的一些依赖包被墙了)

go get -v github.com/spf13/cobra/cobra

所以我们可以首先安装其依赖包:
$GOPATH/src/golang.org/x目录下(如果没有,则自行创建)用git clone下载sys和text项目:

git clone https://github.com/golang/sys

git clone https://github.com/golang/text

然后执行go get -v github.com/spf13/cobra/cobra安装即可。
若成功安装,则在$GOBIN下会出现cobra可执行程序,如果没有配置$GOBIN,则可自行去$GOPATH/bin下寻找该文件。
然后在命令行中输入cobra:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.

Usage:
  cobra [command]

Available Commands:
  add         Add a command to a Cobra Application
  help        Help about any command
  init        Initialize a Cobra Application

如果出现以上提示则表示安装成功。

cobra使用

  1. 生成agenda项目
$ cobra init agenda
  1. 添加agenda工具命令
$ cobra add [命令名称]
  1. cobra工作原理
agenda
  |─ cmd
  |  |─ register
  |  |─ login
  |  ||  └─ ……
  |─ LICENSE
  └─ main.go

main.go文件如下:

package main
import "agenda/cmd"
func main() {
  cmd.Execute()
}

主函数中调用了cmd.Execute(),此函数便启动了整个项目。

cmd/root.go:

var rootCmd = &cobra.Command{
  Use:   "hugo",
  Short: "Hugo is a very fast static site generator",
  Long: `A Fast and Flexible Static Site Generator built with
                love by spf13 and friends in Go.
                Complete documentation is available at http://hugo.spf13.com`,
  Run: func(cmd *cobra.Command, args []string) {
    // Do Stuff Here
  },
}

func Execute() {
  if err := rootCmd.Execute(); err != nil {
    fmt.Println(err)
    os.Exit(1)
  }
}

root.go中定义了Execute()函数,在该函数中,启动了rootCmd.Execute()函数,内部实现中监听了所有命令。

除此之外,我们还可以定义flag来处理命令行参数:

func init() {
  cobra.OnInitialize(initConfig)
  rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra.yaml)")
  rootCmd.PersistentFlags().StringVarP(&projectBase, "projectbase", "b", "", "base project directory eg. github.com/spf13/")
  rootCmd.PersistentFlags().StringP("author", "a", "YOUR NAME", "Author name for copyright attribution")
  rootCmd.PersistentFlags().StringVarP(&userLicense, "license", "l", "", "Name of license for the project (can provide `licensetext` in config)")
  rootCmd.PersistentFlags().Bool("viper", true, "Use Viper for configuration")
  viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
  viper.BindPFlag("projectbase", rootCmd.PersistentFlags().Lookup("projectbase"))
  viper.BindPFlag("useViper", rootCmd.PersistentFlags().Lookup("viper"))
  viper.SetDefault("author", "NAME HERE <EMAIL ADDRESS>")
  viper.SetDefault("license", "apache")
}

上述函数中定义了一些变量,这些变量我在agenda实现中并未接触到,接下来需要进一步学习。

命令介绍

用户注册(agenda register)

  1. 注册新用户时,用户需设置一个唯一的用户名和一个密码。另外,还需登记邮箱及电话信息。
  2. 如果注册时提供的用户名已由其他用户使用,应反馈一个适当的出错信息;成功注册后,亦应反馈一个成功注册的信息。

用户登录(agenda login)

  1. 用户使用用户名和密码登录 Agenda 系统。
  2. 用户名和密码同时正确则登录成功并反馈一个成功登录的信息。否则,登录失败并反馈一个失败登录的信息。

用户登出(agenda logout)

  1. 已登录的用户登出系统后,只能使用用户注册和用户登录功能。

用户查询(agenda userquery)

  1. 已登录的用户可以查看已注册的所有用户的用户名、邮箱及电话信息。

用户删除(agenda ru)

  1. 已登录的用户可以删除本用户账户(即销号)。
  2. 操作成功,需反馈一个成功注销的信息;否则,反馈一个失败注销的信息。
  3. 删除成功则退出系统登录状态。删除后,该用户账户不再存在。
  4. 用户账户删除以后:
    • 以该用户为 发起者 的会议将被删除
    • 以该用户为 参与者 的会议将从 参与者 列表中移除该用户。若因此造成会议 参与者 人数为0,则会议也将被删除。

创建会议(agenda cm)

  1. 已登录的用户可以添加一个新会议到其议程安排中。会议可以在多个已注册 用户间举行,不允许包含未注册用户。添加会议时提供的信息应包括:
    • 会议主题(title)(在会议列表中具有唯一性)
    • 会议发起者(originator)
    • 会议参与者(participator)
    • 会议起始时间(start time)
    • 会议结束时间(end time)
  2. 注意,任何用户都无法分身参加多个会议。如果用户已有的会议安排(作为发起者或参与者)与将要创建的会议在时间上重叠 (允许仅有端点重叠的情况),则无法创建该会议。
  3. 用户应获得适当的反馈信息,以便得知是成功地创建了新会议,还是在创建过程中出现了某些错误。

增删会议参与者(agenda ap/rp)

  1. 已登录的用户可以向 自己发起的某一会议增加/删除 参与者 。
  2. 增加参与者时需要做 时间重叠 判断(允许仅有端点重叠的情况)。
  3. 删除会议参与者后,若因此造成会议 参与者 人数为0,则会议也将被删除。

查询会议(agenda mtquery)

  1. 已登录的用户可以查询自己的议程在某一时间段(time interval)内的所有会议安排。
  2. 用户给出所关注时间段的起始时间和终止时间,返回该用户议程中在指定时间范围内找到的所有会议安排的列表。
  3. 在列表中给出每一会议的起始时间、终止时间、主题、以及发起者和参与者。
  4. 注意,查询会议的结果应包括用户作为 发起者或参与者 的会议。

取消会议(agenda mtcancel)

  1. 已登录的用户可以取消 自己发起 的某一会议安排。
  2. 取消会议时,需提供唯一标识:会议主题(title)。

退出会议(agenda mtquit)

  1. 已登录的用户可以退出 自己参与 的某一会议安排。
  2. 退出会议时,需提供一个唯一标识:会议主题(title)。若因此造成会议 参与者 人数为0,则会议也将被删除。

清空会议(agenda mtclear)

  1. 已登录的用户可以清空 自己发起 的所有会议安排。

文件结构介绍

agenda
├─ cmd
|   |─ ap.go
|   |─ cm.go
|   |─ login.go
|   |─ logout.go
|   |─ mtcancel.go
|   |─ mtquery.go
|   |─ mtquit.go
|   |─ register.go
|   |─ root.go
|   |─ rp.go
|   |─ ru.go
|   └─ userquery.go
├─ docs
|   |─ commandIntro.md
|   |─ entity.md
|   |─ models.md
├─ entity
|   |─ meetingOp.gp
|   |─ userInfoOp.go
|   |─ meetingOp_test.go
|   └─ userInfoOp.go
├─ log
|   └─ logFile.txt
├─ models
|   ├─ logger.go
|   ├─ meeting.go
|   └─ user.go
├─ storage
|   ├─ curUser.txt
|   ├─ meetings.json
|   └─ users.json
├─ LICENSE
├─ main.go
└─ README.md

entity介绍

meetingOp.go

  • ReadMeetingFromFile()

    /**
     * @arguments: nil
     * @return: []models.Meeting
     */
    

    此函数用于获取文件中所存放的所有会议。
    通过利用文件读操作,包括os、bufio、json-iterator/go等库的使用,我们可以解析meetings.json文件中存储的所有会议并作为models.Meeting切片来返回。

  • WriteMeetingToFile()

    /**
     * @arguments: []models.Meeting
     * @return: nil
     */
    

    此函数用于将当前列表中的更新后的所有会议重新写入meetings.json文件中。
    通过利用文件写操作,包括os、bufio、json-iterator/go等库的使用,我们可以将所有会议编码为json格式的字符串并存储到meetings.json文件中。

  • FetchMeetingsByName()

    /**
     * @arguments: name string
     * @return: []models.Meeting
     */
    

    此函数用于根据用户名字来获取该用户参与过的所有会议。
    通过利用文件读操作,包括os、bufio、json-iterator/go等库的使用,我们遍历整个会议文件,查询该用户参加/主持的所有会议作为modles.Meeting切片来返回。

  • RemoveParticipantsByName()

    /**
     * @arguments:  name string, meeting models.Meeting
     * @return: models.Meeting
     */
    

    此函数用于将一个参与人员从一个会议中移除。
    通过传入的meeting来查询该会议中是否包含该参与人员。如果包含,则将改参与人员删除;否则,不进行任何操作。该函数主要是一个中间过程函数,并不将出以后的meeting存储到文件中,在其他函数中可以调用该函数来完成相应的操作之后再存储。

meetingOp.go测试

关于测试,我使用go语言自带的测试框架go test,相关内容不在展开叙述。
首先定义数据:

var meetings = []models.Meeting{
	{
		Title:        "first",
		Originator:   "liuyh73",
		Participants: "liuyh74,liuyh75",
		StartTime:    "2018/11/1 10:00:00",
		EndTime:      "2018/11/1 10:30:00",
	},
	{
		Title:        "second",
		Originator:   "liu",
		Participants: "liuyh73,wang",
		StartTime:    "2018/10/13 21:00:00",
		EndTime:      "2018/10/13 21:30:00",
	},
}
  • ReadMeetingFromFile和WriteMeetingToFile测试
    在测试中,我将二者同时进行测试,首先将上面定义的两个会议写入文件中,然后再从文件中读取。如果读取出的数据与上述数据一致,则证明函数测试正确。
    func TestReadMeetingFromFile_WriteMeetingToFile(t *testing.T) {
        WriteMeetingToFile(meetings)
        meetingsRead := ReadMeetingFromFile()
        if len(meetingsRead) == 2 && meetings[0] == meetingsRead[0] && meetings[1] == meetingsRead[1] {
            t.Log("ReadMeetingFromFile 和 WriteMeetingToFile 测试通过")
        } else {
            t.Error("ReadMeetingFromFile 或者 WriteMeetingToFile 测试失败")
        }
    }
    
  • FetchMeetingsByName测试
    此函数测试,我找寻两组不同的测试案例:一个为已经参加会议的liuyh73,另一个为未参加会议的liuyh76
    func TestFetchMeetingsByName(t *testing.T) {
        meetingsFetchedByName := FetchMeetingsByName("liuyh73")
        meetingsFetchedByName2 := FetchMeetingsByName("liuyh76")
        if len(meetingsFetchedByName) == 2 && len(meetingsFetchedByName2) == 0 {
            t.Log("FetchMeetingsByName 测试通过")
        } else {
            t.Error("FetchMeetingsByName 测试失败")
        }
    }
    
  • RemoveParticipantsByName测试
    此函数测试,我也找寻两组不同的测试案例:一个为参与会议meeting[0]的liuyh74,另一个为未参与会议meeting[1](非主持人员)的liu:
    func TestRemoveParticipantsByName(t *testing.T) {
        meetingRemovedByName := RemoveParticipantsByName("liuyh74", meetings[0])
        meetingRemovedByName2 := RemoveParticipantsByName("liu", meetings[1])
        if (len(strings.Split(meetingRemovedByName.Participants, ",")) == 1 && meetingRemovedByName.Participants == "liuyh75") &&
            (len(strings.Split(meetingRemovedByName2.Participants, ",")) == 2 && meetingRemovedByName2.Participants == "liuyh73,wang") {
            t.Log("RemoveParticipantsByName 测试通过")
        } else {
            t.Error("RemoveParticipantsByName 测试失败")
        }
    }
    

userInfoOp.go

  • ReadUserInfoFromFile()

    /**
     * @arguments: nil
     * @return: []models.User
     */
    

    此函数用于从users.json文件中读取所有用户信息。
    通过利用文件读操作,包括os、bufio、json-iterator/go等库的使用,我们遍历整个用户信息文件,获取所有用户的models.Meeting切片然后返回。

  • WriteUserInfoToFile()

    /**
     * @arguments: []models.User
     * @return: nil
     */
    

    此函数用于将当前列表中的更新后的所有用户信息重新写入users.json文件中。
    通过利用文件写操作,包括os、bufio、json-iterator/go等库的使用,我们可以将所有用户信息编码为json格式的字符串并存储到users.json文件中。

  • SaveCurUserInfo()

    /**
     * @arguments: loginUser models.User
     * @return: nil
     */
    

    此函数用于将当前登陆的用户信息存储到curUser.txt文件中,方便登陆用户信息的存储。

  • ClearCurUserInfo()

    /**
     * @arguments: nil
     * @return: nil
     */
    

    当登陆用户登出的时候,我们利用os库Truncate函数来将登录用户信息从curUser.txt文件中删除。

  • IsLoggedIn()

    /**
     * @arguments: nil
     * @return: bool, models.User
     */
    

    此函数判断当前是否已经已经有用户登录,并且返回登录用户信息。
    我们可以利用此函数来加一些限定,因为未登录的用户不能进行cm、mtcancel等操作。

  • IsUser()

    /**
     * @arguments: name string
     * @return: bool
     */
    

    此函数用于判端当前用户名是否为已注册的用户,调用ReadUserInfoFromFile并加以判断即可。可以用于在创建、删除会议时判断用户是否存在;或者注册用户时判断该用户名是否已经被注册等。

  • RemoveUser()

    /**
     * @arguments: name string
     * @return: nil
     */
    

    此函数用于移除用处,主要是方便ru操作。调用ReadUserInfoFromFile获取用户信息,加以处理后再调用WriteUserInfoToFile更新用户信息即可。

userInfoOp.go测试

首先,定义数据:

var users = []models.User{
	{
		Username:  "liuyh73",
		Password:  "123",
		Telephone: "12364579823",
		Email:     "[email protected]",
	},
	{
		Username:  "liuyh74",
		Password:  "123",
		Telephone: "12364579823",
		Email:     "[email protected]",
	},
}

  • ReadUserInfoFromFile和WriteUserInfoToFile测试
    在测试中,我将二者同时进行测试,首先将上面定义的两个用户写入文件中,然后再从文件中读取。如果读取出的数据与上述数据一致,则证明函数测试正确。
    具体代码与测试会议的代码基本一致,不再赘述。
  • SaveCurUserInfo测试
    假设"liuyh73"为登录用户,将其保存在curUser.txt中,然后再从中读取出数据,若该数据等于users[0],则测试正确。
    func TestSaveCurUserInfo(t *testing.T) {
        // 假设登陆用户为liuyh73
        SaveCurUserInfo(users[0])
        file, err := os.OpenFile(models.ExecPath+"github.com/sysu-615/agenda/storage/curUser.txt", os.O_RDWR|os.O_CREATE, 0644)
        defer file.Close()
    
        if err != nil {
            panic(err)
        }
        var loginUser models.User
        reader := bufio.NewReader(file)
        data, _ := reader.ReadBytes('\n')
    
        jsoniter.Unmarshal(data, &loginUser)
        if loginUser == users[0] {
            t.Log("SaveCurUserInfo 测试通过")
        } else {
            t.Error("SaveCurUserInfo 测试失败")
        }
    }
    
  • IsLoggedIn测试
    判断登陆的用户是否为liuyh73即可。
    func TestIsLoggedIn(t *testing.T) {
        login, loginUser := IsLoggedIn()
        if login && loginUser == users[0] {
            t.Log("IsLoggedIn 测试通过")
        } else {
            t.Error("IsLoggedIn 测试失败")
        }
    }
    
  • ClearCurUserInfo测试
    将当前登陆的用户从curUser.txt中移除,重新读取该文件,若该文件为空,则测试正确。
    func TestClearCurUserInfo(t *testing.T) {
        ClearCurUserInfo()
        file, err := os.OpenFile(models.ExecPath+"github.com/sysu-615/agenda/storage/curUser.txt", os.O_RDWR|os.O_CREATE, 0644)
        defer file.Close()
    
        if err != nil {
            panic(err)
        }
        reader := bufio.NewReader(file)
        data, err := reader.ReadBytes('\n')
        if string(data) == "" && err == io.EOF {
            t.Log("ClearCurUserInfo 测试通过")
        } else {
            t.Error("ClearCurUserInfo 测试失败")
        }
    }
    
  • IsUser测试
    找寻两组数据:一个是已注册用户liuyh73,另一个为未注册用户liuyh75.
    func TestIsUser(t *testing.T) {
        isUser := IsUser("liuyh73")
        isNotUser := IsUser("liuyh75")
        if isUser && !isNotUser {
            t.Log("IsUser 测试通过")
        } else {
            t.Error("IsUser 测试失败")
        }
    }
    
  • RemoveUser测试
    删除用户,然后读取文件获取用户数量,判断是否删除成功。
    func TestRemoveUser(t *testing.T) {
        RemoveUser("liuyh73")
        users := ReadUserInfoFromFile()
        RemoveUser("liuyh74")
        users2 := ReadUserInfoFromFile()
        if len(users) == 1 && len(users2) == 0 {
            t.Log("RemoveUser 测试通过")
        } else {
            t.Error("RemoveUser 测试失败")
        }
    }
    

测试结果

=== RUN   TestReadMeetingFromFile_WriteMeetingToFile
--- PASS: TestReadMeetingFromFile_WriteMeetingToFile (0.00s)
    meetingOp_test.go:31: ReadMeetingFromFile 和 WriteMeetingToFile 测试通过
=== RUN   TestFetchMeetingsByName
--- PASS: TestFetchMeetingsByName (0.00s)
    meetingOp_test.go:41: FetchMeetingsByName 测试通过
=== RUN   TestRemoveParticipantsByName
--- PASS: TestRemoveParticipantsByName (0.00s)
    meetingOp_test.go:52: RemoveParticipantsByName 测试通过
=== RUN   TestReadUserInfoFromFile_WriteUserInfoToFile
--- PASS: TestReadUserInfoFromFile_WriteUserInfoToFile (0.00s)
    userInfoOp_test.go:33: ReadUserInfoFromFile 和 WriteUserInfoToFile 测试通过
=== RUN   TestSaveCurUserInfo
--- PASS: TestSaveCurUserInfo (0.00s)
    userInfoOp_test.go:54: SaveCurUserInfo 测试通过
=== RUN   TestIsLoggedIn
--- PASS: TestIsLoggedIn (0.00s)
    userInfoOp_test.go:63: IsLoggedIn 测试通过
=== RUN   TestClearCurUserInfo
--- PASS: TestClearCurUserInfo (0.00s)
    userInfoOp_test.go:80: ClearCurUserInfo 测试通过
=== RUN   TestIsUser
--- PASS: TestIsUser (0.00s)
    userInfoOp_test.go:90: IsUser 测试通过
=== RUN   TestRemoveUser
--- PASS: TestRemoveUser (0.00s)
    userInfoOp_test.go:102: RemoveUser 测试通过
PASS
ok      github.com/sysu-615/agenda/entity       0.480s

agenda测试

简单测试:(有些命令并没有测试到)

// 注册liuyh73
λ agenda register -uliuyh73 -p123 -P15976541234 [email protected]
Register liuyh73 successfully!

// 登录liuyh73
λ agenda login -uliuyh73 -p123
Login successfully

// 注册liuyt
λ agenda register -uliuyt -p234 -P15912345432 [email protected]
Register liuyt successfully!

// 注册liux
λ agenda register -uliux -p345 -P15978342332 [email protected]
Register liux successfully!

// 登录liuyt
λ agenda login -uliuyt -p234
liuyh73 has already in

// 创建会议test
λ agenda cm -ttest -oliuyh73 -pliuyt,liux -s="2018/11/1 10:20:00" -e="2018/11/1 11:00:00"
Create meeting: test successfully

// 船舰会议时间冲突
λ agenda cm -ttest -oliuyh73 -pliuyt -s="2018/11/1 10:30:00" -e="2018/11/1 11:10:00"
Some meetings of the sponsor("liuyh73")conflict with the meeting in terms of time.

// 创建会议liu未注册
λ agenda cm -ttest -oliuyh73 -pliuyt,liux,liu -s="2018/11/1 10:20:00" -e="2018/11/1 11:00:00"
The participator liu has not yet registered

// 移除参与者liux
λ agenda rp -ttest -pliux
rp called
Remove participators liux from meeting test success!

// 取消会议test
λ agenda mtcancel -ttest
mtcancel called
the meeting test are cancelled!

// 移除用户
λ agenda ru liuyh73
Remove user [liuyh73] successfully

// 退出用户
λ agenda logout
No user login

最终,meetings.json、curUser.txt文件为空,users.json存在以下两条记录:

[
	{"Username":"liuyt","Password":"234","Telephone":"15912345432","Email":"[email protected]"},
	{"Username":"liux","Password":"345","Telephone":"15978342332","Email":"[email protected]"}
]

agenda使用

go get github.com/sysu-615/agenda

之后可以在任意路径下执行agenda register, agenda login……等命令(确保将$GOPATH/bin加入到环境变量中)。

猜你喜欢

转载自blog.csdn.net/liuyh73/article/details/83692699